/*
* Copyright 2006-2017 ICEsoft Technologies Canada Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS
* IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.icepdf.core.pobjects;
import org.icepdf.core.pobjects.fonts.Font;
import org.icepdf.core.pobjects.fonts.FontFile;
import org.icepdf.core.pobjects.security.SecurityManager;
import org.icepdf.core.util.Utils;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>This class represents a PDF Hexadecimal String Object. Hexadecimal String
* objects are written as a sequence of literal characters enclosed in
* angled brackets <>.</p>
*
* @since 2.0
*/
public class HexStringObject implements StringObject {
private static Logger logger =
Logger.getLogger(HexStringObject.class.toString());
// core data used to represent the literal string information
private StringBuilder stringData;
// Reference is need for standard encryption
Reference reference;
/**
* <p>Creates a new hexadecimal string object so that it represents the same
* sequence of bytes as in the bytes argument. In other words, the
* initial content of the hexadecimal string is the characters represented
* by the byte data.</p>
*
* @param bytes array of bytes which will be interpreted as hexadecimal
* data.
*/
public HexStringObject(byte[] bytes) {
this(new StringBuilder(bytes.length).append(new String(bytes)));
}
/**
* <p>Creates a new hexadecimal string object so that it represents the same
* sequence of character data specified by the argument. This constructor should
* only be used in the context of the parser which has leading and ending
* angled brackets which are removed by this method.</p>
*
* @param stringBuffer the initial contents of the hexadecimal string object
*/
public HexStringObject(StringBuilder stringBuffer) {
// remove angled brackets, passed in by parser
stringBuffer.deleteCharAt(0);
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
// append string data
stringData = new StringBuilder(stringBuffer.length());
stringData.append(normalizeHex(stringBuffer, 2).toString());
}
public HexStringObject(String string) {
stringData = new StringBuilder(string.length());
stringData.append(normalizeHex(new StringBuilder(string), 2).toString());
}
/**
* <p>Creates a new literal string object so that it represents the same
* sequence of character data specified by the arguments. The string
* value is assumed to be unencrypted and will be encrypted. The
* method #LiteralStringObject(String string) should be used if the string
* is all ready encrypted. This method is used for creating new
* LiteralStringObject's that are created post document parse. </p>
*
* @param string the initial contents of the literal string object,
* unencrypted.
* @param reference of parent PObject
* @param securityManager security manager used ot encrypt the string.
*/
public HexStringObject(String string, Reference reference,
SecurityManager securityManager) {
// append string data
this.reference = reference;
// convert string data to hex encoded
stringData = encodeHexString(string);
// save and encrypt the hex value. TODO: encryption still not working correctly, likely a -> byte[] error.
stringData = new StringBuilder(encryption(stringData.toString(), false, securityManager));
}
/**
* Encodes the given contents string into a 4 byte hex string. This allows us to easily account for
* mixed encoding of 2-byte and 4 byte string content.
*
* @param contents string to be encoded into hex format.
* @return original content stream with contents encoded in the hex string format.
*/
private StringBuilder encodeHexString(String contents) {
StringBuilder hex = new StringBuilder();
if (contents != null && contents.length() > 0) {
char[] chars = contents.toCharArray();
hex.append("FEFF");
String hexCode;
for (char aChar : chars) {
hexCode = Integer.toHexString((int) aChar);
if (hexCode.length() == 2) {
hexCode = "00" + hexCode;
} else if (hexCode.length() == 1) {
hexCode = "000" + hexCode;
}
hex.append(hexCode);
}
}
return hex;
}
/**
* Gets the integer value of the hexidecimal data specified by the start and
* offset parameters.
*
* @param start the begining index, inclusive
* @param offset the length of bytes to process
* @return unsigned integer value of the specifed data range
*/
public int getUnsignedInt(int start, int offset) {
if (start < 0 || stringData.length() < (start + offset))
return 0;
int unsignedInt = 0;
try {
unsignedInt = Integer.parseInt(
stringData.substring(start, start + offset), 16);
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Number Format Exception " + unsignedInt);
}
}
return unsignedInt;
}
public int getUnsignedInt(String data) {
int unsignedInt = 0;
try {
unsignedInt = Integer.parseInt(data, 16);
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Number Format Exception " + unsignedInt);
}
}
return unsignedInt;
}
/**
* <p>Returns a string representation of the object.
* The hex data is converted to an equivalent string representation</p>
*
* @return a string representing the object.
*/
public String toString() {
return getLiteralString();
}
/**
* <p>Gets a hexadecimal String representation of this object's data, which
* is in fact, the raw data contained in this object</p>
*
* @return a String representation of the object's data in hexadecimal notation.
*/
public String getHexString() {
return stringData.toString();
}
/**
* <p>Gets a hexadecimal StringBuffer representation of this object's data,
* which is in fact the raw data contained in this object.</p>
*
* @return a StringBufffer representation of the objects data in hexadecimal.
*/
public StringBuilder getHexStringBuffer() {
return stringData;
}
/**
* <p>Gets a literal StringBuffer representation of this object's data.
* The hexadecimal data is converted to an equivalent string representation</p>
*
* @return a StringBuffer representation of the object's data.
*/
public StringBuilder getLiteralStringBuffer() {
return hexToString(stringData);
}
/**
* <p>Gets a literal String representation of this object's data.
* The hexadecimal data is converted to an equivalent string representation.</p>
*
* @return a String representation of the object's data.
*/
public String getLiteralString() {
return hexToString(stringData).toString();
}
/**
* <p>Gets a literal String representation of this object's data using the
* specifed font and format. The font is used to verify that the
* specific character codes can be rendered; if they can not, they may be
* removed or combined with the next character code to get a displayable
* character code.
*
* @param fontFormat the type of font which will be used to display
* the text. Valid values are CID_FORMAT and SIMPLE_FORMAT for Adobe
* Composite and Simple font types respectively
* @param font font used to render the literal string data.
* @return StringBuffer which contains all renderaable characters for the
* given font.
*/
public StringBuilder getLiteralStringBuffer(final int fontFormat, FontFile font) {
if (fontFormat == Font.SIMPLE_FORMAT) {
stringData = new StringBuilder(normalizeHex(stringData, 2).toString());
int charOffset = 2;
int length = getLength();
StringBuilder tmp = new StringBuilder(length);
int lastIndex = 0;
int charValue;
int offset;
for (int i = 0; i < length; i += charOffset) {
offset = lastIndex + charOffset;
charValue = getUnsignedInt(i - lastIndex, offset);
// 0 cid is valid, so we have ot be careful we don't exclude the
// cid 00 = 0 or 0000 = 0, not 0000 = 00.
if (!(offset < length && charValue == 0) &&
font.canDisplayEchar((char) charValue)) {
tmp.append((char) charValue);
lastIndex = 0;
} else {
lastIndex += charOffset;
}
}
return tmp;
} else if (fontFormat == Font.CID_FORMAT) {
stringData = new StringBuilder(normalizeHex(stringData, 4).toString());
int charOffset = 2;
int length = getLength();
int charValue;
StringBuilder tmp = new StringBuilder(length);
// attempt to detect mulibyte encoded strings.
for (int i = 0; i < length; i += charOffset) {
String first = stringData.substring(i, i + 2);
if (first.charAt(0) != '0') {
// check range for possible 2 byte char ie mixed mode.
charValue = getUnsignedInt(first);
if (font.getByteEncoding() == FontFile.ByteEncoding.MIXED_BYTE &&
font.canDisplayEchar((char) charValue) && font.getSource() != null) {
tmp.append((char) charValue);
} else {
charValue = getUnsignedInt(i, 4);
if (font.canDisplayEchar((char) charValue)) {
tmp.append((char) charValue);
i += 2;
}
}
} else {
charValue = getUnsignedInt(i, 4);
// should never have a 4 digit zero value.
if (font.canDisplayEchar((char) charValue)) {
tmp.append((char) charValue);
i += 2;
}
}
}
return tmp;
}
return null;
}
/**
* The length of the underlying objects data.
*
* @return length of object's data.
*/
public int getLength() {
return stringData.length();
}
/**
* Utility method to removed all none hex character from the string and
* ensure that the length is an even length.
*
* @param hex hex data to normalize
* @param step 2 or 4 character codes.
* @return normalized pure hex StringBuffer
*/
private static StringBuilder normalizeHex(StringBuilder hex, int step) {
// strip and white space
int length = hex.length();
for (int i = 0; i < length; i++) {
if (isNoneHexChar(hex.charAt(i))) {
hex.deleteCharAt(i);
length--;
i--;
}
}
length = hex.length();
if (step == 2) {
// pre append 0's to uneven length, be careful as the 0020 isn't the same as 2000
if (length % 2 != 0) {
hex = new StringBuilder("0").append(hex);
}
}
if (step == 4) {
if (length % 4 != 0) {
hex = new StringBuilder("00").append(hex);
}
}
return hex;
}
/**
* Utility method to test if the char is a none hexadecimal char.
*
* @param c charact to text
* @return true if the character is a none hexadecimal character
*/
private static boolean isNoneHexChar(char c) {
// make sure the char is the following
return !(((c >= 48) && (c <= 57)) || // 0-9
((c >= 65) && (c <= 70)) || // A-F
((c >= 97) && (c <= 102))); // a-f
}
/**
* Utility method for converting a hexadecimal string to a literal string.
*
* @param hh StringBuffer containing data in hexadecimal form.
* @return StringBuffer containing data in literal form.
*/
private StringBuilder hexToString(StringBuilder hh) {
// make sure we have a valid hex value to convert to string.
// can't decrypt an empty string.
if (hh != null && hh.length() == 0) {
return new StringBuilder();
}
StringBuilder sb;
// special case, test for not a 4 byte character code format
if (!((hh.charAt(0) == 'F' | hh.charAt(0) == 'f')
&& (hh.charAt(1) == 'E' | hh.charAt(1) == 'e')
&& (hh.charAt(2) == 'F' | hh.charAt(2) == 'f')
&& (hh.charAt(3) == 'F') | hh.charAt(3) == 'f')) {
return getRawHexToString();
}
// otherwise, assume 4 byte character codes
else {
int length = hh.length();
sb = new StringBuilder(length / 4);
String subStr;
// make sure to skip the marker
for (int i = 4; i < length; i = i + 4) {
subStr = hh.substring(i, i + 4);
sb.append((char) Integer.parseInt(subStr, 16));
}
return sb;
}
}
/**
* Gets the raw string values not taking into account any special cases for FEFF byte
* marking.
*
* @return two byte hex string converted to plain string.
*/
public StringBuilder getRawHexToString() {
StringBuilder sb;
int length = stringData.length();
sb = new StringBuilder(length / 2);
String subStr;
for (int i = 0; i < length; i = i + 2) {
subStr = stringData.substring(i, i + 2);
sb.append((char) Integer.parseInt(subStr, 16));
}
return sb;
}
/**
* Sets the parent PDF object's reference.
*
* @param reference parent object reference.
*/
public void setReference(Reference reference) {
this.reference = reference;
}
/**
* Sets the parent PDF object's reference.
*
* @return returns the reference used for encryption.
*/
public Reference getReference() {
return reference;
}
/**
* Gets the decrypted literal string value of the data using the key provided by the
* security manager.
*
* @param securityManager security manager associated with parent document.
*/
public String getDecryptedLiteralString(SecurityManager securityManager) {
// get the security manager instance
if (securityManager != null && reference != null) {
// get the key
byte[] key = securityManager.getDecryptionKey();
// convert string to bytes.
byte[] textBytes =
Utils.convertByteCharSequenceToByteArray(getLiteralString());
// Decrypt String
textBytes = securityManager.decrypt(reference,
key,
textBytes);
// convert back to a string
return Utils.convertByteArrayToByteString(textBytes);
}
return getLiteralString();
}
/**
* Decrypts or encrypts a string.
*
* @param string string to encrypt or decrypt
* @param decrypt true to decrypt string, false otherwise;
* @param securityManager security manager for document.
* @return encrypted or decrypted string, depends on value of decrypt param.
*/
public String encryption(String string, boolean decrypt,
SecurityManager securityManager) {
// get the security manager instance
if (securityManager != null && reference != null) {
// get the key
byte[] key = securityManager.getDecryptionKey();
// convert string to bytes.
byte[] textBytes =
Utils.convertByteCharSequenceToByteArray(string);
// Decrypt String
if (decrypt) {
textBytes = securityManager.decrypt(reference,
key,
textBytes);
} else {
textBytes = securityManager.encrypt(reference,
key,
textBytes);
}
// convert back to a string
return Utils.convertByteArrayToByteString(textBytes);
}
return string;
}
}