| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You 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.apache.poi.hsmf.datatypes; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.function.Consumer; |
| |
| import org.apache.commons.codec.digest.PureJavaCrc32; |
| import org.apache.poi.hpsf.ClassID; |
| import org.apache.poi.util.LittleEndian; |
| import org.apache.poi.util.LittleEndianByteArrayInputStream; |
| import org.apache.poi.util.StringUtil; |
| |
| /** |
| * Collection of convenience chunks for the NameID part of an outlook file |
| */ |
| public final class NameIdChunks implements ChunkGroup { |
| public static final String NAME = "__nameid_version1.0"; |
| |
| public enum PropertySetType { |
| PS_MAPI("00020328-0000-0000-C000-000000000046"), |
| PS_PUBLIC_STRINGS("00020329-0000-0000-C000-000000000046"), |
| PS_INTERNET_HEADERS("00020386-0000-0000-C000-000000000046"); |
| |
| private final ClassID classID; |
| PropertySetType(String uuid) { |
| classID = new ClassID(uuid); |
| } |
| |
| public ClassID getClassID() { |
| return classID; |
| } |
| } |
| |
| public enum PredefinedPropertySet { |
| PSETID_COMMON("00062008-0000-0000-C000-000000000046"), |
| PSETID_ADDRESS("00062004-0000-0000-C000-000000000046"), |
| PSETID_APPOINTMENT("00062002-0000-0000-C000-000000000046"), |
| PSETID_MEETING("6ED8DA90-450B-101B-98DA-00AA003F1305"), |
| PSETID_LOG("0006200A-0000-0000-C000-000000000046"), |
| PSETID_MESSAGING("41F28F13-83F4-4114-A584-EEDB5A6B0BFF"), |
| PSETID_NOTE("0006200E-0000-0000-C000-000000000046"), |
| PSETID_POST_RSS("00062041-0000-0000-C000-000000000046"), |
| PSETID_TASK("00062003-0000-0000-C000-000000000046"), |
| PSETID_UNIFIED_MESSAGING("4442858E-A9E3-4E80-B900-317A210CC15B"), |
| PSETID_AIR_SYNC("71035549-0739-4DCB-9163-00F0580DBBDF"), |
| PSETID_SHARING("00062040-0000-0000-C000-000000000046"), |
| PSETID_XML_EXTRACTED_ENTITIES("23239608-685D-4732-9C55-4C95CB4E8E33"), |
| PSETID_ATTACHMENT("96357F7F-59E1-47D0-99A7-46515C183B54"); |
| |
| private final ClassID classID; |
| PredefinedPropertySet(String uuid) { |
| classID = new ClassID(uuid); |
| } |
| |
| public ClassID getClassID() { |
| return classID; |
| } |
| } |
| |
| private ByteChunk guidStream; |
| private ByteChunk entryStream; |
| private ByteChunk stringStream; |
| |
| /** Holds all the chunks that were found. */ |
| private List<Chunk> allChunks = new ArrayList<>(); |
| |
| public Chunk[] getAll() { |
| return allChunks.toArray(new Chunk[0]); |
| } |
| |
| @Override |
| public Chunk[] getChunks() { |
| return getAll(); |
| } |
| |
| /** |
| * Called by the parser whenever a chunk is found. |
| */ |
| @Override |
| public void record(Chunk chunk) { |
| if (chunk.getType() == Types.BINARY) { |
| switch (chunk.getChunkId()) { |
| case 2: |
| guidStream = (ByteChunk)chunk; |
| break; |
| case 3: |
| entryStream = (ByteChunk)chunk; |
| break; |
| case 4: |
| stringStream = (ByteChunk)chunk; |
| break; |
| } |
| } |
| allChunks.add(chunk); |
| } |
| |
| /** |
| * Used to flag that all the chunks of the NameID have now been located. |
| */ |
| @Override |
| public void chunksComplete() { |
| // Currently, we don't need to do anything special once |
| // all the chunks have been located |
| } |
| |
| /** |
| * Get property tag id by property set GUID and string name or numerical name from named properties mapping |
| * @param guid Property set GUID in registry format without brackets. |
| * May be one of the PS_* or PSETID_* constants |
| * @param name Property name in case of string named property |
| * @param id Property id in case of numerical named property |
| * @return Property tag which can be matched with {@link org.apache.poi.hsmf.datatypes.MAPIProperty#id} |
| * or 0 if the property could not be found. |
| * |
| */ |
| public long getPropertyTag(ClassID guid, String name, long id) { |
| final byte[] entryStreamBytes = (entryStream == null) ? null : entryStream.getValue(); |
| if (guidStream == null || entryStream == null || stringStream == null || guid == null || |
| entryStreamBytes == null) { |
| return 0; |
| } |
| |
| LittleEndianByteArrayInputStream leis = new LittleEndianByteArrayInputStream(entryStreamBytes); |
| for (int i = 0; i < entryStreamBytes.length / 8; i++) { |
| final long nameOffset = leis.readUInt(); |
| int guidIndex = leis.readUShort(); |
| final int propertyKind = guidIndex & 0x01; |
| guidIndex = guidIndex >>> 1; |
| final int propertyIndex = leis.readUShort(); |
| |
| // fetch and match property GUID |
| if (!guid.equals(getPropertyGUID(guidIndex))) { |
| continue; |
| } |
| |
| // fetch property name / stream ID |
| final String[] propertyName = { null }; |
| final long[] propertyNameCRC32 = { -1L }; |
| long streamID = getStreamID(propertyKind, (int)nameOffset, guid, guidIndex, |
| n -> propertyName[0] = n, c -> propertyNameCRC32[0] = c); |
| |
| if (!matchesProperty(propertyKind, nameOffset, name, propertyName[0], id)) { |
| continue; |
| } |
| |
| // find property index in matching stream entry |
| if (propertyKind == 1 && propertyNameCRC32[0] < 0) { |
| // skip stream entry matching and return tag from property index from entry stream |
| // this code should not be reached |
| return 0x8000L + propertyIndex; |
| } |
| |
| return getPropertyTag(streamID, nameOffset, propertyNameCRC32[0]); |
| } |
| return 0; |
| } |
| |
| private long getPropertyTag(long streamID, long nameOffset, long propertyNameCRC32) { |
| for (Chunk chunk : allChunks) { |
| if (chunk.getType() != Types.BINARY || chunk.getChunkId() != streamID) { |
| continue; |
| } |
| byte[] matchChunkBytes = ((ByteChunk) chunk).getValue(); |
| if (matchChunkBytes == null) { |
| continue; |
| } |
| LittleEndianByteArrayInputStream leis = new LittleEndianByteArrayInputStream(matchChunkBytes); |
| for (int m = 0; m < matchChunkBytes.length / 8; m++) { |
| long nameCRC = leis.readUInt(); |
| int matchGuidIndex = leis.readUShort(); |
| int matchPropertyIndex = leis.readUShort(); |
| int matchPropertyKind = matchGuidIndex & 0x01; |
| |
| if (nameCRC == (matchPropertyKind == 0 ? nameOffset : propertyNameCRC32)) { |
| return 0x8000L + matchPropertyIndex; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| private ClassID getPropertyGUID(int guidIndex) { |
| if (guidIndex == 1) { |
| // predefined GUID |
| return PropertySetType.PS_MAPI.classID; |
| } else if (guidIndex == 2) { |
| // predefined GUID |
| return PropertySetType.PS_PUBLIC_STRINGS.classID; |
| } else if (guidIndex >= 3) { |
| // GUID from guid stream |
| byte[] guidStreamBytes = guidStream.getValue(); |
| int guidIndexOffset = (guidIndex - 3) * 0x10; |
| if (guidStreamBytes.length >= guidIndexOffset + 0x10) { |
| return new ClassID(guidStreamBytes, guidIndexOffset); |
| } |
| } |
| return null; |
| } |
| |
| // property set GUID matches |
| private static boolean matchesProperty(int propertyKind, long nameOffset, String name, String propertyName, long id) { |
| return |
| // match property by id |
| (propertyKind == 0 && id >= 0 && id == nameOffset) || |
| // match property by name |
| (propertyKind == 1 && name != null && name.equals(propertyName)); |
| } |
| |
| |
| private long getStreamID(int propertyKind, int nameOffset, ClassID guid, int guidIndex, |
| Consumer<String> propertyNameSetter, Consumer<Long> propertyNameCRC32Setter) { |
| if (propertyKind == 0) { |
| // numerical named property |
| return 0x1000L + (nameOffset ^ (guidIndex << 1)) % 0x1F; |
| } |
| |
| // string named property |
| byte[] stringBytes = stringStream.getValue(); |
| long propertyNameCRC32 = -1; |
| if (stringBytes.length > nameOffset) { |
| long nameLength = LittleEndian.getUInt(stringBytes, nameOffset); |
| if (stringBytes.length >= nameOffset + 4 + nameLength) { |
| int nameStart = nameOffset + 4; |
| String propertyName = new String(stringBytes, nameStart, (int) nameLength, StringUtil.UTF16LE); |
| if (PropertySetType.PS_INTERNET_HEADERS.classID.equals(guid)) { |
| byte[] n = propertyName.toLowerCase(Locale.ROOT).getBytes(StringUtil.UTF16LE); |
| propertyNameCRC32 = calculateCRC32(n, 0, n.length); |
| } else { |
| propertyNameCRC32 = calculateCRC32(stringBytes, nameStart, (int)nameLength); |
| } |
| propertyNameSetter.accept(propertyName); |
| propertyNameCRC32Setter.accept(propertyNameCRC32); |
| } |
| } |
| return 0x1000 + (propertyNameCRC32 ^ ((guidIndex << 1) | 1)) % 0x1F; |
| } |
| |
| /** |
| * Calculates the CRC32 of the given bytes (conforms to RFC 1510, SSH-1). |
| * The CRC32 calculation is similar to the standard one as demonstrated in RFC 1952, |
| * but with the inversion (before and after the calculation) omitted. |
| * <ul> |
| * <li>poly: 0x04C11DB7</li> |
| * <li>init: 0x00000000</li> |
| * <li>xor: 0x00000000</li> |
| * <li>revin: true</li> |
| * <li>revout: true</li> |
| * <li>check: 0x2DFD2D88 (CRC32 of "123456789")</li> |
| * </ul> |
| * |
| * @param buf the byte array to calculate CRC32 on |
| * @param off the offset within buf at which the CRC32 calculation will start |
| * @param len the number of bytes on which to calculate the CRC32 |
| * @return the CRC32 value (unsigned 32-bit integer stored in a long). |
| * |
| * @see <a href="http://www.zorc.breitbandkatze.de/crc.html">CRC parameter check</a> |
| */ |
| private static long calculateCRC32(byte[] buf, int off, int len) { |
| PureJavaCrc32 crc = new PureJavaCrc32(); |
| // set initial crc value to 0 |
| crc.update( new byte[] {-1,-1,-1,-1}, 0, 4); |
| crc.update(buf, off, len); |
| return ~crc.getValue() & 0xFFFFFFFFL; |
| } |
| |
| } |