/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * RecordOutputStream.java * Created: Feb 25, 2005 * By: Raymond Cypher */ package org.openquark.cal.internal.serialization; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UTFDataFormatException; import java.util.LinkedHashMap; import org.openquark.cal.compiler.ModuleName; import org.openquark.cal.compiler.QualifiedName; import org.openquark.cal.util.ArrayStack; /** * Stream class for writing records. */ public class RecordOutputStream implements DataOutput { private static final int recordPoolSchema = 0; /** The byte which is XORed against each byte in a UTF-8 encoded string. */ /* @implementation Keep this below 0x7f (to avoid sign extension issues)! Set this to 0 to leave the strings unobfuscated. */ static final byte OBFUSCATING_BYTE = (byte)0x5c; // Backing output stream. This isn't actually written to until the // RecordOutputStream is closed. private final OutputStream output; // Stack to keep track of the current record. private final ArrayStack<LabelledByteArrayOutputStream.Label> recordStack = ArrayStack.make (); // As strings are saved we build up a pool of unique strings and // actually save the key into this pool. The pool is written // at the beginning of the output when close is called. private final LinkedHashMap<String, Integer> stringPool = new LinkedHashMap<String, Integer>(); // As QualifiedNames are saved we build up a pool of unique instances and // actually save the key into this pool. The pool is written // at the beginning of the output when close is called. private final LinkedHashMap<QualifiedName, Integer> qnPool = new LinkedHashMap<QualifiedName, Integer>(); // Pool the schemas for the different record types, keyed by record tag. //private LinkedHashMap schemaPool = new LinkedHashMap(); // The fully buffered repositionable backing stream used to write to. private final LabelledByteArrayOutputStream out = new LabelledByteArrayOutputStream (); private final byte writeBuffer[] = new byte[8]; public RecordOutputStream (OutputStream output) { this.output = output; } /** * Start a record with the given tag and schema. * @param tag * @param schema * @throws IOException */ public void startRecord (short tag, int schema) throws IOException { writeShortCompressed(tag); writeShortCompressed (schema); recordStack.push (out.getLabel()); // Reserve space for the length value. writeInt(0); } /** * End the current record. * @throws IOException */ public void endRecord () throws IOException { LabelledByteArrayOutputStream.Label label = recordStack.pop (); int length = out.size() - label.getStartPosition() - 4; int nLengthBytes = RecordOutputStream.writeIntCompressed(length, new DataOutputStream (label)); if (nLengthBytes == 2) { out.shiftBackBy(label.getStartPosition()+4, 2, length); } } /** * Writes the specified byte (the low eight bits of the argument * <code>b</code>) to the underlying output stream. If no exception * is thrown, the counter <code>written</code> is incremented by * <code>1</code>. * <p> * Implements the <code>write</code> method of <code>OutputStream</code>. * * @param b the <code>byte</code> to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public synchronized void write(int b) throws IOException { out.write(b); } /** * Writes <code>len</code> bytes from the specified byte array * starting at offset <code>off</code> to the underlying output stream. * If no exception is thrown, the counter <code>written</code> is * incremented by <code>len</code>. * * @param b the data. * @param off the start offset in the data. * @param len the number of bytes to write. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public synchronized void write(byte b[], int off, int len) throws IOException { out.write(b, off, len); } public void write(byte b[]) throws IOException { write(b, 0, b.length); } /** * When we close this stream we want to write the buffered output * to the backing output stream. This involves the pooled values * maintained by the RecordOutputStream being written out first. * @throws IOException */ public void close () throws IOException { try { // Write out the pools. startRecord(ModuleSerializationTags.POOLED_VALUES, recordPoolSchema); // Write out the string pool. writeIntCompressed(stringPool.size()); for (final String key : stringPool.keySet()) { writeUTFInternal(key); } // Write out the QualifiedName pool. writeIntCompressed(qnPool.size()); for (final QualifiedName qn : qnPool.keySet()) { writeIntCompressed(getStringIndex(qn.getModuleName().toSourceText())); writeIntCompressed(getStringIndex(qn.getUnqualifiedName())); } endRecord(); // Now write out the content of the RecordOutputStream. out.writeTo(output); output.flush(); } finally { output.close(); } } /** * Writes a <code>boolean</code> to the underlying output stream as * a 1-byte value. The value <code>true</code> is written out as the * value <code>(byte)1</code>; the value <code>false</code> is * written out as the value <code>(byte)0</code>. If no exception is * thrown, the counter <code>written</code> is incremented by * <code>1</code>. * * @param v a <code>boolean</code> value to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeBoolean(boolean v) throws IOException { out.write(v ? 1 : 0); } /** * Writes out a <code>byte</code> to the underlying output stream as * a 1-byte value. If no exception is thrown, the counter * <code>written</code> is incremented by <code>1</code>. * * @param v a <code>byte</code> value to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeByte(int v) throws IOException { out.write(v); } /** * Writes a <code>short</code> to the underlying output stream as two * bytes, high byte first. If no exception is thrown, the counter * <code>written</code> is incremented by <code>2</code>. * * @param v a <code>short</code> to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeShort(int v) throws IOException { out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); } /** * Writes a <code>char</code> to the underlying output stream as a * 2-byte value, high byte first. If no exception is thrown, the * counter <code>written</code> is incremented by <code>2</code>. * * @param v a <code>char</code> value to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeChar(int v) throws IOException { out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); } /** * Writes an <code>int</code> to the underlying output stream as four * bytes, high byte first. If no exception is thrown, the counter * <code>written</code> is incremented by <code>4</code>. * * @param v an <code>int</code> to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeInt(int v) throws IOException { out.write((v >>> 24) & 0xFF); out.write((v >>> 16) & 0xFF); out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); // if(((v >>> 24) & 0xFF) == 0 && ((v >>> 16) & 0xFF) == 0) { // nWastedBytes2 += 2; // } } public final int writeIntCompressed(int v) throws IOException { return RecordOutputStream.writeIntCompressed(v, this); } public final void writeShortCompressed(int shortVal) throws IOException { RecordOutputStream.writeShortCompressed(shortVal, this); } private static final void writeShortCompressed(int shortVal, DataOutput d) throws IOException { if (shortVal < 0) { throw new IOException ("Unable to compress 32 bit integer with value < 0."); } if (shortVal > Short.MAX_VALUE) { throw new IOException ("Unable to fit int value of " + shortVal + " into two bytes."); } if (shortVal > Byte.MAX_VALUE) { shortVal |= 0x8000; d.writeShort(shortVal); } else { d.writeByte(shortVal); } } private static int writeIntCompressed(int v, DataOutput d) throws IOException { if (v < 0) { throw new IOException ("Unable to compress 32 bit integer with value < 0."); } if (v > Short.MAX_VALUE) { v |= 0x80000000; d.writeInt(v); return 4; } else { d.writeShort(v); return 2; } } /** * Writes a <code>long</code> to the underlying output stream as eight * bytes, high byte first. In no exception is thrown, the counter * <code>written</code> is incremented by <code>8</code>. * * @param v a <code>long</code> to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeLong(long v) throws IOException { writeBuffer[0] = (byte)(v >>> 56); writeBuffer[1] = (byte)(v >>> 48); writeBuffer[2] = (byte)(v >>> 40); writeBuffer[3] = (byte)(v >>> 32); writeBuffer[4] = (byte)(v >>> 24); writeBuffer[5] = (byte)(v >>> 16); writeBuffer[6] = (byte)(v >>> 8); writeBuffer[7] = (byte)(v >>> 0); out.write(writeBuffer, 0, 8); } /** * Converts the float argument to an <code>int</code> using the * <code>floatToIntBits</code> method in class <code>Float</code>, * and then writes that <code>int</code> value to the underlying * output stream as a 4-byte quantity, high byte first. If no * exception is thrown, the counter <code>written</code> is * incremented by <code>4</code>. * * @param v a <code>float</code> value to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out * @see java.lang.Float#floatToIntBits(float) */ public final void writeFloat(float v) throws IOException { writeInt(Float.floatToIntBits(v)); } /** * Converts the double argument to a <code>long</code> using the * <code>doubleToLongBits</code> method in class <code>Double</code>, * and then writes that <code>long</code> value to the underlying * output stream as an 8-byte quantity, high byte first. If no * exception is thrown, the counter <code>written</code> is * incremented by <code>8</code>. * * @param v a <code>double</code> value to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out * @see java.lang.Double#doubleToLongBits(double) */ public final void writeDouble(double v) throws IOException { writeLong(Double.doubleToLongBits(v)); } /** * Writes out the string to the underlying output stream as a * sequence of bytes. Each character in the string is written out, in * sequence, by discarding its high eight bits. If no exception is * thrown, the counter <code>written</code> is incremented by the * length of <code>s</code>. * * @param s a string of bytes to be written. * @exception IOException if an I/O error occurs. * @see RecordOutputStream#out */ public final void writeBytes(String s) throws IOException { int len = s.length(); for (int i = 0 ; i < len ; i++) { out.write((byte)s.charAt(i)); } } /** * Writes a string to the underlying output stream as a sequence of * characters. Each character is written to the data output stream as * if by the <code>writeChar</code> method. If no exception is * thrown, the counter <code>written</code> is incremented by twice * the length of <code>s</code>. * * @param s a <code>String</code> value to be written. * @exception IOException if an I/O error occurs. * @see java.io.DataOutputStream#writeChar(int) * @see RecordOutputStream#out */ public final void writeChars(String s) throws IOException { int len = s.length(); for (int i = 0 ; i < len ; i++) { int v = s.charAt(i); out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); } } /** * Writes a string to the underlying output stream using Java modified * UTF-8 encoding in a machine-independent manner. * <p> * First, two bytes are written to the output stream as if by the * <code>writeShort</code> method giving the number of bytes to * follow. This value is the number of bytes actually written out, * not the length of the string. Following the length, each character * of the string is output, in sequence, using the modified UTF-8 encoding * for the character. If no exception is thrown, the counter * <code>written</code> is incremented by the total number of * bytes written to the output stream. This will be at least two * plus the length of <code>str</code>, and at most two plus * thrice the length of <code>str</code>. * * @param str a string to be written. * @exception IOException if an I/O error occurs. */ public final void writeUTF(String str) throws IOException { writeIntCompressed(getStringIndex(str)); } private int getStringIndex(String str) { if (str == null) { return 0; } Integer index = stringPool.get(str); if (index == null) { index = Integer.valueOf(stringPool.size() + 1); stringPool.put(str, index); } return index.intValue(); } /** * Serialize a string ignoring the string pooling mechanism. * Strings serialized using this function must be read * via RecordInputStream.readUTFUnPooled(). * @param string * @throws IOException */ public void writeUTFUnPooled (String string) throws IOException { writeUTFInternal(string); } /** * Writes a string to the underlying output stream using Java modified * UTF-8 encoding in a machine-independent manner. * <p> * First, two bytes are written to the output stream as if by the * <code>writeShort</code> method giving the number of bytes to * follow. This value is the number of bytes actually written out, * not the length of the string. Following the length, each character * of the string is output, in sequence, using the modified UTF-8 encoding * for the character. If no exception is thrown, the counter * <code>written</code> is incremented by the total number of * bytes written to the output stream. This will be at least two * plus the length of <code>str</code>, and at most two plus * thrice the length of <code>str</code>. * * @param str a string to be written. * @exception IOException if an I/O error occurs. */ private final void writeUTFInternal(String str) throws IOException { writeUTF(str, this); } /** * Writes a string to the specified DataOutput using Java modified UTF-8 * encoding in a machine-independent manner. * <p> * First, two bytes are written to out as if by the <code>writeShort</code> * method giving the number of bytes to follow. This value is the number of * bytes actually written out, not the length of the string. Following the * length, each character of the string is output, in sequence, using the * modified UTF-8 encoding for the character. If no exception is thrown, the * counter <code>written</code> is incremented by the total number of * bytes written to the output stream. This will be at least two * plus the length of <code>str</code>, and at most two plus * thrice the length of <code>str</code>. * * @param str a string to be written. * @param out destination to write to * @return The number of bytes written out. * @exception IOException if an I/O error occurs. */ private static int writeUTF(String str, DataOutput out) throws IOException { int strlen = str.length(); int utflen = 0; char[] charr = new char[strlen]; int c, count = 0; str.getChars(0, strlen, charr, 0); for (int i = 0; i < strlen; i++) { c = charr[i]; if ((c >= 0x0001) && (c <= 0x007F)) { utflen++; } else if (c > 0x07FF) { utflen += 3; } else { utflen += 2; } } if (utflen > 65535) { throw new UTFDataFormatException(); } byte[] bytearr = new byte[utflen+2]; // The first 2 bytes contain the length of the string and does not need to be obfuscated bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF); bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF); for (int i = 0; i < strlen; i++) { c = charr[i]; if ((c >= 0x0001) && (c <= 0x007F)) { bytearr[count++] = (byte) (c ^ OBFUSCATING_BYTE); } else if (c > 0x07FF) { bytearr[count++] = (byte) ((0xE0 | ((c >> 12) & 0x0F)) ^ OBFUSCATING_BYTE); bytearr[count++] = (byte) ((0x80 | ((c >> 6) & 0x3F)) ^ OBFUSCATING_BYTE); bytearr[count++] = (byte) ((0x80 | ((c >> 0) & 0x3F)) ^ OBFUSCATING_BYTE); } else { bytearr[count++] = (byte) ((0xC0 | ((c >> 6) & 0x1F)) ^ OBFUSCATING_BYTE); bytearr[count++] = (byte) ((0x80 | ((c >> 0) & 0x3F)) ^ OBFUSCATING_BYTE); } } out.write(bytearr); return utflen + 2; } /** * Writes a module name to the RecordOutputStream. * @param moduleName a module name, may not be null. * @throws IOException */ public void writeModuleName(ModuleName moduleName) throws IOException { writeUTF(moduleName.toSourceText()); } /** * Write a qualified name to the RecordOutputStream. * @param qualifiedName may be null. * @throws IOException */ public void writeQualifiedName(QualifiedName qualifiedName) throws IOException { if (qualifiedName == null) { writeIntCompressed (0); } else { Integer qnIndex = qnPool.get(qualifiedName); if (qnIndex == null) { qnIndex = Integer.valueOf(qnPool.size() + 1); qnPool.put(qualifiedName, qnIndex); } // Force the strings that make up the qualified name to // be added to the string pool. getStringIndex(qualifiedName.getModuleName().toSourceText()); getStringIndex(qualifiedName.getUnqualifiedName()); writeIntCompressed(qnIndex.intValue()); } } public static byte[] booleanArrayToBitArray(boolean[] b) { int nBytes = (b.length + 7) / 8; byte[] bytes = new byte[nBytes]; for (int i = 0; i < b.length; i++) { if (b[i]) { int iByte = i / 8; int iBit = i % 8; bytes[iByte] |= (0x01 << iBit); } } return bytes; } }