You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PropertiesChunk.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.hsmf.datatypes;
  16. import java.io.ByteArrayInputStream;
  17. import java.io.ByteArrayOutputStream;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.OutputStream;
  21. import java.util.ArrayList;
  22. import java.util.Collections;
  23. import java.util.HashMap;
  24. import java.util.List;
  25. import java.util.Locale;
  26. import java.util.Map;
  27. import java.util.Map.Entry;
  28. import org.apache.poi.hsmf.datatypes.PropertyValue.BooleanPropertyValue;
  29. import org.apache.poi.hsmf.datatypes.PropertyValue.CurrencyPropertyValue;
  30. import org.apache.poi.hsmf.datatypes.PropertyValue.DoublePropertyValue;
  31. import org.apache.poi.hsmf.datatypes.PropertyValue.FloatPropertyValue;
  32. import org.apache.poi.hsmf.datatypes.PropertyValue.LongLongPropertyValue;
  33. import org.apache.poi.hsmf.datatypes.PropertyValue.LongPropertyValue;
  34. import org.apache.poi.hsmf.datatypes.PropertyValue.NullPropertyValue;
  35. import org.apache.poi.hsmf.datatypes.PropertyValue.ShortPropertyValue;
  36. import org.apache.poi.hsmf.datatypes.PropertyValue.TimePropertyValue;
  37. import org.apache.poi.hsmf.datatypes.Types.MAPIType;
  38. import org.apache.poi.poifs.filesystem.DirectoryEntry;
  39. import org.apache.poi.util.IOUtils;
  40. import org.apache.poi.util.LittleEndian;
  41. import org.apache.poi.util.LittleEndian.BufferUnderrunException;
  42. import org.apache.poi.util.POILogFactory;
  43. import org.apache.poi.util.POILogger;
  44. /**
  45. * <p>
  46. * A Chunk which holds (single) fixed-length properties, and pointer to the
  47. * variable length ones / multi-valued ones (which get their own chunk).
  48. * <p>
  49. * There are two kinds of PropertiesChunks, which differ only in their headers.
  50. */
  51. public abstract class PropertiesChunk extends Chunk {
  52. public static final String NAME = "__properties_version1.0";
  53. // arbitrarily selected; may need to increase
  54. private static final int MAX_RECORD_LENGTH = 1_000_000;
  55. // standard prefix, defined in the spec
  56. public static final String VARIABLE_LENGTH_PROPERTY_PREFIX = "__substg1.0_";
  57. // standard property flags, defined in the spec
  58. public static final int PROPERTIES_FLAG_READABLE = 2;
  59. public static final int PROPERTIES_FLAG_WRITEABLE = 4;
  60. /** For logging problems we spot with the file */
  61. private POILogger logger = POILogFactory.getLogger(PropertiesChunk.class);
  62. /**
  63. * Holds properties, indexed by type. If a property is multi-valued, or
  64. * variable length, it will be held via a {@link ChunkBasedPropertyValue}.
  65. */
  66. private Map<MAPIProperty, PropertyValue> properties = new HashMap<>();
  67. /**
  68. * The ChunkGroup that these properties apply to. Used when matching chunks
  69. * to variable sized and multi-valued properties
  70. */
  71. private ChunkGroup parentGroup;
  72. /**
  73. * Creates a Properties Chunk.
  74. */
  75. protected PropertiesChunk(ChunkGroup parentGroup) {
  76. super(NAME, -1, Types.UNKNOWN);
  77. this.parentGroup = parentGroup;
  78. }
  79. @Override
  80. public String getEntryName() {
  81. return NAME;
  82. }
  83. /**
  84. * Returns all the properties in the chunk, without looking up any
  85. * chunk-based values
  86. */
  87. public Map<MAPIProperty, PropertyValue> getRawProperties() {
  88. return properties;
  89. }
  90. /**
  91. * <p>
  92. * Returns all the properties in the chunk, along with their values.
  93. * <p>
  94. * Any chunk-based values will be looked up and returned as such
  95. */
  96. public Map<MAPIProperty, List<PropertyValue>> getProperties() {
  97. Map<MAPIProperty, List<PropertyValue>> props =
  98. new HashMap<>(properties.size());
  99. for (MAPIProperty prop : properties.keySet()) {
  100. props.put(prop, getValues(prop));
  101. }
  102. return props;
  103. }
  104. /**
  105. * Defines a property. Multi-valued properties are not yet supported.
  106. */
  107. public void setProperty(PropertyValue value) {
  108. properties.put(value.getProperty(), value);
  109. }
  110. /**
  111. * Returns all values for the given property, looking up chunk based ones as
  112. * required, of null if none exist
  113. */
  114. public List<PropertyValue> getValues(MAPIProperty property) {
  115. PropertyValue val = properties.get(property);
  116. if (val == null) {
  117. return null;
  118. }
  119. if (val instanceof ChunkBasedPropertyValue) {
  120. // ChunkBasedPropertyValue cval = (ChunkBasedPropertyValue)val;
  121. // TODO Lookup
  122. return Collections.emptyList();
  123. } else {
  124. return Collections.singletonList(val);
  125. }
  126. }
  127. /**
  128. * Returns the value / pointer to the value chunk of the property, or null
  129. * if none exists
  130. */
  131. public PropertyValue getRawValue(MAPIProperty property) {
  132. return properties.get(property);
  133. }
  134. /**
  135. * Called once the parent ChunkGroup has been populated, to match up the
  136. * Chunks in it with our Variable Sized Properties.
  137. */
  138. protected void matchVariableSizedPropertiesToChunks() {
  139. // Index the Parent Group chunks for easy lookup
  140. // TODO Is this the right way?
  141. Map<Integer, Chunk> chunks = new HashMap<>();
  142. for (Chunk chunk : parentGroup.getChunks()) {
  143. chunks.put(chunk.getChunkId(), chunk);
  144. }
  145. // Loop over our values, looking for chunk based ones
  146. for (PropertyValue val : properties.values()) {
  147. if (val instanceof ChunkBasedPropertyValue) {
  148. ChunkBasedPropertyValue cVal = (ChunkBasedPropertyValue) val;
  149. Chunk chunk = chunks.get(cVal.getProperty().id);
  150. // System.err.println(cVal.getProperty() + " = " + cVal + " -> "
  151. // + HexDump.toHex(cVal.data));
  152. // TODO Make sense of the raw offset value
  153. if (chunk != null) {
  154. cVal.setValue(chunk);
  155. } else {
  156. logger.log(POILogger.WARN, "No chunk found matching Property " + cVal);
  157. }
  158. }
  159. }
  160. }
  161. protected void readProperties(InputStream value) throws IOException {
  162. boolean going = true;
  163. while (going) {
  164. try {
  165. // Read in the header
  166. int typeID = LittleEndian.readUShort(value);
  167. int id = LittleEndian.readUShort(value);
  168. long flags = LittleEndian.readUInt(value);
  169. // Turn the Type and ID into helper objects
  170. MAPIType type = Types.getById(typeID);
  171. MAPIProperty prop = MAPIProperty.get(id);
  172. // Wrap properties we don't know about as custom ones
  173. if (prop == MAPIProperty.UNKNOWN) {
  174. prop = MAPIProperty.createCustom(id, type, "Unknown " + id);
  175. }
  176. if (type == null) {
  177. logger.log(POILogger.WARN, "Invalid type found, expected ",
  178. prop.usualType, " but got ", typeID,
  179. " for property ", prop);
  180. going = false;
  181. break;
  182. }
  183. // Sanity check the property's type against the value's type
  184. if (prop.usualType != type) {
  185. // Is it an allowed substitution?
  186. if (type == Types.ASCII_STRING
  187. && prop.usualType == Types.UNICODE_STRING
  188. || type == Types.UNICODE_STRING
  189. && prop.usualType == Types.ASCII_STRING) {
  190. // It's fine to go with the specified instead of the
  191. // normal
  192. } else if (prop.usualType == Types.UNKNOWN) {
  193. // We don't know what this property normally is, but it
  194. // has come
  195. // through with a valid type, so use that
  196. logger.log(POILogger.INFO, "Property definition for ", prop,
  197. " is missing a type definition, found a value with type ", type);
  198. } else {
  199. // Oh dear, something has gone wrong...
  200. logger.log(POILogger.WARN, "Type mismatch, expected ",
  201. prop.usualType, " but got ", type, " for property ", prop);
  202. going = false;
  203. break;
  204. }
  205. }
  206. // TODO Detect if it is multi-valued, since if it is
  207. // then even fixed-length strings store their multiple
  208. // values in another chunk (much as variable length ones)
  209. // Work out how long the "data" is
  210. // This might be the actual data, or just a pointer
  211. // to another chunk which holds the data itself
  212. boolean isPointer = false;
  213. int length = type.getLength();
  214. if (!type.isFixedLength()) {
  215. isPointer = true;
  216. length = 8;
  217. }
  218. // Grab the data block
  219. byte[] data = IOUtils.safelyAllocate(length, MAX_RECORD_LENGTH);
  220. IOUtils.readFully(value, data);
  221. // Skip over any padding
  222. if (length < 8) {
  223. byte[] padding = new byte[8 - length];
  224. IOUtils.readFully(value, padding);
  225. }
  226. // Wrap and store
  227. PropertyValue propVal = null;
  228. if (isPointer) {
  229. // We'll match up the chunk later
  230. propVal = new ChunkBasedPropertyValue(prop, flags, data, type);
  231. } else if (type == Types.NULL) {
  232. propVal = new NullPropertyValue(prop, flags, data);
  233. } else if (type == Types.BOOLEAN) {
  234. propVal = new BooleanPropertyValue(prop, flags, data);
  235. } else if (type == Types.SHORT) {
  236. propVal = new ShortPropertyValue(prop, flags, data);
  237. } else if (type == Types.LONG) {
  238. propVal = new LongPropertyValue(prop, flags, data);
  239. } else if (type == Types.LONG_LONG) {
  240. propVal = new LongLongPropertyValue(prop, flags, data);
  241. } else if (type == Types.FLOAT) {
  242. propVal = new FloatPropertyValue(prop, flags, data);
  243. } else if (type == Types.DOUBLE) {
  244. propVal = new DoublePropertyValue(prop, flags, data);
  245. } else if (type == Types.CURRENCY) {
  246. propVal = new CurrencyPropertyValue(prop, flags, data);
  247. } else if (type == Types.TIME) {
  248. propVal = new TimePropertyValue(prop, flags, data);
  249. }
  250. // TODO Add in the rest of the types
  251. else {
  252. propVal = new PropertyValue(prop, flags, data, type);
  253. }
  254. if (properties.get(prop) != null) {
  255. logger.log(POILogger.WARN,
  256. "Duplicate values found for " + prop);
  257. }
  258. properties.put(prop, propVal);
  259. } catch (BufferUnderrunException e) {
  260. // Invalid property, ended short
  261. going = false;
  262. }
  263. }
  264. }
  265. /**
  266. * Writes this chunk in the specified {@code DirectoryEntry}.
  267. *
  268. * @param directory
  269. * The directory.
  270. * @throws IOException
  271. * If an I/O error occurs.
  272. */
  273. public void writeProperties(DirectoryEntry directory) throws IOException {
  274. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  275. List<PropertyValue> values = writeProperties(baos);
  276. baos.close();
  277. // write the header data with the properties declaration
  278. directory.createDocument(org.apache.poi.hsmf.datatypes.PropertiesChunk.NAME,
  279. new ByteArrayInputStream(baos.toByteArray()));
  280. // write the property values
  281. writeNodeData(directory, values);
  282. }
  283. /**
  284. * Write the nodes for variable-length data. Those properties are returned by
  285. * {@link #writeProperties(java.io.OutputStream)}.
  286. *
  287. * @param directory
  288. * The directory.
  289. * @param values
  290. * The values.
  291. * @throws IOException
  292. * If an I/O error occurs.
  293. */
  294. protected void writeNodeData(DirectoryEntry directory, List<PropertyValue> values) throws IOException {
  295. for (PropertyValue value : values) {
  296. byte[] bytes = value.getRawValue();
  297. String nodeName = VARIABLE_LENGTH_PROPERTY_PREFIX + getFileName(value.getProperty(), value.getActualType());
  298. directory.createDocument(nodeName, new ByteArrayInputStream(bytes));
  299. }
  300. }
  301. /**
  302. * Writes the header of the properties.
  303. *
  304. * @param out
  305. * The {@code OutputStream}.
  306. * @return The variable-length properties that need to be written in another
  307. * node.
  308. * @throws IOException
  309. * If an I/O error occurs.
  310. */
  311. protected List<PropertyValue> writeProperties(OutputStream out) throws IOException {
  312. List<PropertyValue> variableLengthProperties = new ArrayList<>();
  313. for (Entry<MAPIProperty, PropertyValue> entry : properties.entrySet()) {
  314. MAPIProperty property = entry.getKey();
  315. PropertyValue value = entry.getValue();
  316. if (value == null) {
  317. continue;
  318. }
  319. if (property.id < 0) {
  320. continue;
  321. }
  322. // generic header
  323. // page 23, point 2.4.2
  324. // tag is the property id and its type
  325. long tag = Long.parseLong(getFileName(property, value.getActualType()), 16);
  326. LittleEndian.putUInt(tag, out);
  327. LittleEndian.putUInt(value.getFlags(), out); // readable + writable
  328. MAPIType type = getTypeMapping(value.getActualType());
  329. if (type.isFixedLength()) {
  330. // page 11, point 2.1.2
  331. writeFixedLengthValueHeader(out, property, type, value);
  332. } else {
  333. // page 12, point 2.1.3
  334. writeVariableLengthValueHeader(out, property, type, value);
  335. variableLengthProperties.add(value);
  336. }
  337. }
  338. return variableLengthProperties;
  339. }
  340. private void writeFixedLengthValueHeader(OutputStream out, MAPIProperty property, MAPIType type, PropertyValue value) throws IOException {
  341. // fixed type header
  342. // page 24, point 2.4.2.1.1
  343. byte[] bytes = value.getRawValue();
  344. int length = bytes != null ? bytes.length : 0;
  345. if (bytes != null) {
  346. out.write(bytes);
  347. }
  348. out.write(new byte[8 - length]);
  349. }
  350. private void writeVariableLengthValueHeader(OutputStream out, MAPIProperty propertyEx, MAPIType type,
  351. PropertyValue value) throws IOException {
  352. // variable length header
  353. // page 24, point 2.4.2.2
  354. byte[] bytes = value.getRawValue();
  355. int length = bytes != null ? bytes.length : 0;
  356. // alter the length, as specified in page 25
  357. if (type == Types.UNICODE_STRING) {
  358. length += 2;
  359. } else if (type == Types.ASCII_STRING) {
  360. length += 1;
  361. }
  362. LittleEndian.putUInt(length, out);
  363. // specified in page 25
  364. LittleEndian.putUInt(0, out);
  365. }
  366. private String getFileName(MAPIProperty property, MAPIType actualType) {
  367. String str = Integer.toHexString(property.id).toUpperCase(Locale.ROOT);
  368. while (str.length() < 4) {
  369. str = "0" + str;
  370. }
  371. MAPIType type = getTypeMapping(actualType);
  372. return str + type.asFileEnding();
  373. }
  374. private MAPIType getTypeMapping(MAPIType type) {
  375. return type == Types.ASCII_STRING ? Types.UNICODE_STRING : type;
  376. }
  377. }