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.

CurrentUserAtom.java 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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.hslf.record;
  16. import static org.apache.logging.log4j.util.Unbox.box;
  17. import static org.apache.poi.hslf.record.RecordAtom.getMaxRecordLength;
  18. import static org.apache.poi.hslf.usermodel.HSLFSlideShow.PP95_DOCUMENT;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.OutputStream;
  22. import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
  23. import org.apache.logging.log4j.LogManager;
  24. import org.apache.logging.log4j.Logger;
  25. import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
  26. import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
  27. import org.apache.poi.poifs.filesystem.DirectoryNode;
  28. import org.apache.poi.poifs.filesystem.DocumentEntry;
  29. import org.apache.poi.poifs.filesystem.Entry;
  30. import org.apache.poi.poifs.filesystem.POIFSFileSystem;
  31. import org.apache.poi.util.IOUtils;
  32. import org.apache.poi.util.LittleEndian;
  33. import org.apache.poi.util.StringUtil;
  34. /**
  35. * This is a special kind of Atom, because it doesn't live inside the
  36. * PowerPoint document. Instead, it lives in a separate stream in the
  37. * document. As such, it has to be treated specially
  38. */
  39. public class CurrentUserAtom {
  40. private static final Logger LOG = LogManager.getLogger(CurrentUserAtom.class);
  41. /** Standard Atom header */
  42. private static final byte[] atomHeader = new byte[] { 0, 0, -10, 15 };
  43. /** The PowerPoint magic number for a non-encrypted file */
  44. private static final byte[] headerToken = new byte[] { 95, -64, -111, -29 };
  45. /** The PowerPoint magic number for an encrypted file */
  46. private static final byte[] encHeaderToken = new byte[] { -33, -60, -47, -13 };
  47. // The Powerpoint 97 version, major and minor numbers
  48. // byte[] ppt97FileVer = new byte[] { 8, 00, -13, 03, 03, 00 };
  49. /** The version, major and minor numbers */
  50. private int docFinalVersion;
  51. private byte docMajorNo;
  52. private byte docMinorNo;
  53. /** The Offset into the file for the current edit */
  54. private long currentEditOffset;
  55. /** The Username of the last person to edit the file */
  56. private String lastEditUser;
  57. /** The document release version. Almost always 8 */
  58. private long releaseVersion;
  59. /** Only correct after reading in or writing out */
  60. private byte[] _contents;
  61. /** Flag for encryption state of the whole file */
  62. private boolean isEncrypted;
  63. /* ********************* getter/setter follows *********************** */
  64. public int getDocFinalVersion() { return docFinalVersion; }
  65. public byte getDocMajorNo() { return docMajorNo; }
  66. public byte getDocMinorNo() { return docMinorNo; }
  67. public long getReleaseVersion() { return releaseVersion; }
  68. public void setReleaseVersion(long rv) { releaseVersion = rv; }
  69. /** Points to the UserEditAtom */
  70. public long getCurrentEditOffset() { return currentEditOffset; }
  71. public void setCurrentEditOffset(long id ) { currentEditOffset = id; }
  72. public String getLastEditUsername() { return lastEditUser; }
  73. public void setLastEditUsername(String u) { lastEditUser = u; }
  74. public boolean isEncrypted() { return isEncrypted; }
  75. public void setEncrypted(boolean isEncrypted) { this.isEncrypted = isEncrypted; }
  76. /* ********************* real code follows *************************** */
  77. /**
  78. * Create a new Current User Atom
  79. */
  80. public CurrentUserAtom() {
  81. _contents = new byte[0];
  82. // Initialise to empty
  83. docFinalVersion = 0x03f4;
  84. docMajorNo = 3;
  85. docMinorNo = 0;
  86. releaseVersion = 8;
  87. currentEditOffset = 0;
  88. lastEditUser = "Apache POI";
  89. isEncrypted = false;
  90. }
  91. /**
  92. * Find the Current User in the filesystem, and create from that
  93. */
  94. public CurrentUserAtom(DirectoryNode dir) throws IOException {
  95. // Decide how big it is
  96. final Entry entry = dir.getEntry("Current User");
  97. if (!(entry instanceof DocumentEntry)) {
  98. throw new IllegalArgumentException("Had unexpected type of entry for name: Current User: " + entry.getClass());
  99. }
  100. DocumentEntry docProps = (DocumentEntry) entry;
  101. // If it's clearly junk, bail out
  102. if(docProps.getSize() > 131072) {
  103. throw new CorruptPowerPointFileException("The Current User stream is implausably long. It's normally 28-200 bytes long, but was " + docProps.getSize() + " bytes");
  104. }
  105. // Grab the contents
  106. try (InputStream in = dir.createDocumentInputStream("Current User")) {
  107. _contents = IOUtils.toByteArray(in, docProps.getSize(), getMaxRecordLength());
  108. }
  109. // See how long it is. If it's under 28 bytes long, we can't
  110. // read it
  111. if(_contents.length < 28) {
  112. boolean isPP95 = dir.hasEntry(PP95_DOCUMENT);
  113. // PPT95 has 4 byte size, then data
  114. if (!isPP95 && _contents.length >= 4) {
  115. int size = LittleEndian.getInt(_contents);
  116. isPP95 = (size + 4 == _contents.length);
  117. }
  118. if (isPP95) {
  119. throw new OldPowerPointFormatException("Based on the Current User stream, you seem to have supplied a PowerPoint95 file, which isn't supported");
  120. } else {
  121. throw new CorruptPowerPointFileException("The Current User stream must be at least 28 bytes long, but was only " + _contents.length);
  122. }
  123. }
  124. // Set everything up
  125. init();
  126. }
  127. /**
  128. * Actually do the creation from a block of bytes
  129. */
  130. private void init() {
  131. // First up is the size, in 4 bytes, which is fixed
  132. // Then is the header
  133. isEncrypted = (LittleEndian.getInt(encHeaderToken) == LittleEndian.getInt(_contents,12));
  134. // Grab the edit offset
  135. currentEditOffset = LittleEndian.getUInt(_contents,16);
  136. // Grab the versions
  137. docFinalVersion = LittleEndian.getUShort(_contents,22);
  138. docMajorNo = _contents[24];
  139. docMinorNo = _contents[25];
  140. // Get the username length
  141. long usernameLen = LittleEndian.getUShort(_contents,20);
  142. if(usernameLen > 512) {
  143. // Handle the case of it being garbage
  144. LOG.atWarn().log("Invalid username length {} found, treating as if there was no username set", box(usernameLen));
  145. usernameLen = 0;
  146. }
  147. // Now we know the length of the username,
  148. // use this to grab the revision
  149. if(_contents.length >= 28+(int)usernameLen + 4) {
  150. releaseVersion = LittleEndian.getUInt(_contents,28+(int)usernameLen);
  151. } else {
  152. // No revision given, as not enough data. Odd
  153. releaseVersion = 0;
  154. }
  155. // Grab the unicode username, if stored
  156. int start = 28+(int)usernameLen+4;
  157. if(_contents.length >= start+2*usernameLen) {
  158. lastEditUser = StringUtil.getFromUnicodeLE(_contents, start, (int)usernameLen);
  159. } else {
  160. // Fake from the 8 bit version
  161. lastEditUser = StringUtil.getFromCompressedUnicode(_contents, 28, (int)usernameLen);
  162. }
  163. }
  164. /**
  165. * Writes ourselves back out
  166. */
  167. public void writeOut(OutputStream out) throws IOException {
  168. // Decide on the size
  169. // 8 = atom header
  170. // 20 = up to name
  171. // 4 = revision
  172. // 3 * len = ascii + unicode
  173. int size = 8 + 20 + 4 + (3 * lastEditUser.length());
  174. _contents = IOUtils.safelyAllocate(size, getMaxRecordLength());
  175. // First we have a 8 byte atom header
  176. System.arraycopy(atomHeader,0,_contents,0,4);
  177. // Size is 20+user len + revision len(4)
  178. int atomSize = 20+4+lastEditUser.length();
  179. LittleEndian.putInt(_contents,4,atomSize);
  180. // Now we have the size of the details, which is 20
  181. LittleEndian.putInt(_contents,8,20);
  182. // Now the ppt un-encrypted header token (4 bytes)
  183. System.arraycopy((isEncrypted ? encHeaderToken : headerToken),0,_contents,12,4);
  184. // Now the current edit offset
  185. LittleEndian.putInt(_contents,16,(int)currentEditOffset);
  186. // The username gets stored twice, once as US
  187. // ascii, and again as unicode laster on
  188. byte[] asciiUN = IOUtils.safelyAllocate(lastEditUser.length(), getMaxRecordLength());
  189. StringUtil.putCompressedUnicode(lastEditUser,asciiUN,0);
  190. // Now we're able to do the length of the last edited user
  191. LittleEndian.putShort(_contents,20,(short)asciiUN.length);
  192. // Now the file versions, 2+1+1
  193. LittleEndian.putShort(_contents,22,(short)docFinalVersion);
  194. _contents[24] = docMajorNo;
  195. _contents[25] = docMinorNo;
  196. // 2 bytes blank
  197. _contents[26] = 0;
  198. _contents[27] = 0;
  199. // At this point we have the username as us ascii
  200. System.arraycopy(asciiUN,0,_contents,28,asciiUN.length);
  201. // 4 byte release version
  202. LittleEndian.putInt(_contents,28+asciiUN.length,(int)releaseVersion);
  203. // username in unicode
  204. byte [] ucUN = IOUtils.safelyAllocate(lastEditUser.length() * 2L, getMaxRecordLength());
  205. StringUtil.putUnicodeLE(lastEditUser,ucUN,0);
  206. System.arraycopy(ucUN,0,_contents,28+asciiUN.length+4,ucUN.length);
  207. // Write out
  208. out.write(_contents);
  209. }
  210. /**
  211. * Writes ourselves back out to a filesystem
  212. */
  213. public void writeToFS(POIFSFileSystem fs) throws IOException {
  214. // Grab contents
  215. try (UnsynchronizedByteArrayOutputStream baos = UnsynchronizedByteArrayOutputStream.builder().get()) {
  216. writeOut(baos);
  217. try (InputStream is = baos.toInputStream()) {
  218. // Write out
  219. fs.createOrUpdateDocument(is, "Current User");
  220. }
  221. }
  222. }
  223. }