Autopsy  4.16.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
SevenZipExtractor.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2013-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.modules.embeddedfileextractor;
20 
21 import java.io.File;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.nio.charset.Charset;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Date;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.logging.Level;
35 import net.sf.sevenzipjbinding.ArchiveFormat;
36 import static net.sf.sevenzipjbinding.ArchiveFormat.RAR;
37 import net.sf.sevenzipjbinding.ExtractAskMode;
38 import net.sf.sevenzipjbinding.ExtractOperationResult;
39 import net.sf.sevenzipjbinding.IArchiveExtractCallback;
40 import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
41 import net.sf.sevenzipjbinding.IInArchive;
42 import net.sf.sevenzipjbinding.ISequentialOutStream;
43 import net.sf.sevenzipjbinding.PropID;
44 import net.sf.sevenzipjbinding.SevenZip;
45 import net.sf.sevenzipjbinding.SevenZipException;
46 import net.sf.sevenzipjbinding.SevenZipNativeInitializationException;
47 import org.apache.tika.parser.txt.CharsetDetector;
48 import org.apache.tika.parser.txt.CharsetMatch;
49 import org.netbeans.api.progress.ProgressHandle;
50 import org.openide.util.NbBundle;
51 import org.openide.util.NbBundle.Messages;
64 import org.sleuthkit.datamodel.AbstractFile;
65 import org.sleuthkit.datamodel.Blackboard;
66 import org.sleuthkit.datamodel.BlackboardArtifact;
67 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT;
68 import org.sleuthkit.datamodel.BlackboardAttribute;
69 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT;
70 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION;
71 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME;
72 import org.sleuthkit.datamodel.Content;
73 import org.sleuthkit.datamodel.DerivedFile;
74 import org.sleuthkit.datamodel.EncodedFileOutputStream;
75 import org.sleuthkit.datamodel.ReadContentInputStream;
76 import org.sleuthkit.datamodel.TskCoreException;
77 import org.sleuthkit.datamodel.TskData;
78 
79 class SevenZipExtractor {
80 
81  private static final Logger logger = Logger.getLogger(SevenZipExtractor.class.getName());
82 
83  private static final String MODULE_NAME = EmbeddedFileExtractorModuleFactory.getModuleName();
84 
85  //encryption type strings
86  private static final String ENCRYPTION_FILE_LEVEL = NbBundle.getMessage(EmbeddedFileExtractorIngestModule.class,
87  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFileLevel");
88  private static final String ENCRYPTION_FULL = NbBundle.getMessage(EmbeddedFileExtractorIngestModule.class,
89  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.encryptionFull");
90 
91  //zip bomb detection
92  private static final int MAX_DEPTH = 4;
93  private static final int MAX_COMPRESSION_RATIO = 600;
94  private static final long MIN_COMPRESSION_RATIO_SIZE = 500 * 1000000L;
95  private static final long MIN_FREE_DISK_SPACE = 1 * 1000 * 1000000L; //1GB
96 
97  private IngestServices services = IngestServices.getInstance();
98  private final IngestJobContext context;
99  private final FileTypeDetector fileTypeDetector;
100 
101  private String moduleDirRelative;
102  private String moduleDirAbsolute;
103 
104  private Blackboard blackboard;
105 
106  private ProgressHandle progress;
107  private int numItems;
108  private String currentArchiveName;
109 
110  private String getLocalRootAbsPath(String uniqueArchiveFileName) {
111  return moduleDirAbsolute + File.separator + uniqueArchiveFileName;
112  }
113 
118 
119  ZIP("application/zip"), //NON-NLS
120  SEVENZ("application/x-7z-compressed"), //NON-NLS
121  GZIP("application/gzip"), //NON-NLS
122  XGZIP("application/x-gzip"), //NON-NLS
123  XBZIP2("application/x-bzip2"), //NON-NLS
124  XTAR("application/x-tar"), //NON-NLS
125  XGTAR("application/x-gtar"),
126  XRAR("application/x-rar-compressed"); //NON-NLS
127 
128  private final String mimeType;
129 
130  SupportedArchiveExtractionFormats(final String mimeType) {
131  this.mimeType = mimeType;
132  }
133 
134  @Override
135  public String toString() {
136  return this.mimeType;
137  }
138  // TODO Expand to support more formats after upgrading Tika
139  }
140 
141  SevenZipExtractor(IngestJobContext context, FileTypeDetector fileTypeDetector, String moduleDirRelative, String moduleDirAbsolute) throws SevenZipNativeInitializationException {
142  if (!SevenZip.isInitializedSuccessfully()) {
143  throw new SevenZipNativeInitializationException("SevenZip has not been previously initialized.");
144  }
145 
146  this.context = context;
147  this.fileTypeDetector = fileTypeDetector;
148  this.moduleDirRelative = moduleDirRelative;
149  this.moduleDirAbsolute = moduleDirAbsolute;
150  }
151 
160  boolean isSevenZipExtractionSupported(AbstractFile file) {
161  String fileMimeType = fileTypeDetector.getMIMEType(file);
162  for (SupportedArchiveExtractionFormats mimeType : SupportedArchiveExtractionFormats.values()) {
163  if (mimeType.toString().equals(fileMimeType)) {
164  return true;
165  }
166  }
167  return false;
168  }
169 
193  private boolean isZipBombArchiveItemCheck(AbstractFile archiveFile, IInArchive inArchive, int inArchiveItemIndex, ConcurrentHashMap<Long, Archive> depthMap, String escapedFilePath) {
194  //If a file is corrupted as a result of reconstructing it from unallocated space, then
195  //7zip does a poor job estimating the original uncompressed file size.
196  //As a result, many corrupted files have wonky compression ratios and could flood the UI
197  //with false zip bomb notifications. The decision was made to skip compression ratio checks
198  //for unallocated zip files. Instead, we let the depth be an indicator of a zip bomb.
199  //Gzip archives compress a single file. They may have a sparse file,
200  //and that file could be much larger, however it won't be the exponential growth seen with more dangerous zip bombs.
201  //In addition a fair number of browser cache files will be gzip archives,
202  //and their file sizes are frequently retrieved incorrectly so ignoring gzip files is a reasonable decision.
203  if (archiveFile.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.UNALLOC) || archiveFile.getMIMEType().equalsIgnoreCase(SupportedArchiveExtractionFormats.XGZIP.toString())) {
204  return false;
205  }
206 
207  try {
208  final Long archiveItemSize = (Long) inArchive.getProperty(
209  inArchiveItemIndex, PropID.SIZE);
210 
211  //skip the check for small files
212  if (archiveItemSize == null || archiveItemSize < MIN_COMPRESSION_RATIO_SIZE) {
213  return false;
214  }
215 
216  final Long archiveItemPackedSize = (Long) inArchive.getProperty(
217  inArchiveItemIndex, PropID.PACKED_SIZE);
218 
219  if (archiveItemPackedSize == null || archiveItemPackedSize <= 0) {
220  logger.log(Level.WARNING, "Cannot getting compression ratio, cannot detect if zipbomb: {0}, item: {1}", //NON-NLS
221  new Object[]{archiveFile.getName(), (String) inArchive.getProperty(inArchiveItemIndex, PropID.PATH)}); //NON-NLS
222  return false;
223  }
224 
225  int cRatio = (int) (archiveItemSize / archiveItemPackedSize);
226 
227  if (cRatio >= MAX_COMPRESSION_RATIO) {
228  Archive rootArchive = depthMap.get(depthMap.get(archiveFile.getId()).getRootArchiveId());
229  String details = NbBundle.getMessage(SevenZipExtractor.class,
230  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.isZipBombCheck.warnDetails",
231  cRatio, FileUtil.escapeFileName(getArchiveFilePath(rootArchive.getArchiveFile())));
232 
233  flagRootArchiveAsZipBomb(rootArchive, archiveFile, details, escapedFilePath);
234  return true;
235  } else {
236  return false;
237  }
238 
239  } catch (SevenZipException ex) {
240  logger.log(Level.WARNING, "Error getting archive item size and cannot detect if zipbomb. ", ex); //NON-NLS
241  return false;
242  }
243  }
244 
256  private void flagRootArchiveAsZipBomb(Archive rootArchive, AbstractFile archiveFile, String details, String escapedFilePath) {
257  rootArchive.flagAsZipBomb();
258  logger.log(Level.INFO, details);
259  try {
260  Collection<BlackboardAttribute> attributes = Arrays.asList(
261  new BlackboardAttribute(
262  TSK_SET_NAME, MODULE_NAME,
263  "Possible Zip Bomb"),
264  new BlackboardAttribute(
265  TSK_DESCRIPTION, MODULE_NAME,
266  Bundle.SevenZipExtractor_zipBombArtifactCreation_text(archiveFile.getName())),
267  new BlackboardAttribute(
268  TSK_COMMENT, MODULE_NAME,
269  details));
270 
271  if (!blackboard.artifactExists(archiveFile, TSK_INTERESTING_FILE_HIT, attributes)) {
272  BlackboardArtifact artifact = rootArchive.getArchiveFile().newArtifact(TSK_INTERESTING_FILE_HIT);
273  artifact.addAttributes(attributes);
274  try {
275  /*
276  * post the artifact which will index the artifact for
277  * keyword search, and fire an event to notify UI of this
278  * new artifact
279  */
280  blackboard.postArtifact(artifact, MODULE_NAME);
281 
282  String msg = NbBundle.getMessage(SevenZipExtractor.class,
283  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.isZipBombCheck.warnMsg", archiveFile.getName(), escapedFilePath);//NON-NLS
284 
285  services.postMessage(IngestMessage.createWarningMessage(MODULE_NAME, msg, details));
286 
287  } catch (Blackboard.BlackboardException ex) {
288  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
289  MessageNotifyUtil.Notify.error(
290  Bundle.SevenZipExtractor_indexError_message(), artifact.getDisplayName());
291  }
292  }
293  } catch (TskCoreException ex) {
294  logger.log(Level.SEVERE, "Error creating blackboard artifact for Zip Bomb Detection for file: " + escapedFilePath, ex); //NON-NLS
295  }
296  }
297 
306  private ArchiveFormat get7ZipOptions(AbstractFile archiveFile) {
307  // try to get the file type from the BB
308  String detectedFormat;
309  detectedFormat = archiveFile.getMIMEType();
310 
311  if (detectedFormat == null) {
312  logger.log(Level.WARNING, "Could not detect format for file: {0}", archiveFile); //NON-NLS
313 
314  // if we don't have attribute info then use file extension
315  String extension = archiveFile.getNameExtension();
316  if ("rar".equals(extension)) //NON-NLS
317  {
318  // for RAR files we need to open them explicitly as RAR. Otherwise, if there is a ZIP archive inside RAR archive
319  // it will be opened incorrectly when using 7zip's built-in auto-detect functionality
320  return RAR;
321  }
322 
323  // Otherwise open the archive using 7zip's built-in auto-detect functionality
324  return null;
325  } else if (detectedFormat.contains("application/x-rar-compressed")) //NON-NLS
326  {
327  // for RAR files we need to open them explicitly as RAR. Otherwise, if there is a ZIP archive inside RAR archive
328  // it will be opened incorrectly when using 7zip's built-in auto-detect functionality
329  return RAR;
330  }
331 
332  // Otherwise open the archive using 7zip's built-in auto-detect functionality
333  return null;
334  }
335 
346  private long getRootArchiveId(AbstractFile file) throws TskCoreException {
347  long id = file.getId();
348  Content parentContent = file.getParent();
349  while (parentContent != null) {
350  id = parentContent.getId();
351  parentContent = parentContent.getParent();
352  }
353  return id;
354  }
355 
370  private List<AbstractFile> getAlreadyExtractedFiles(AbstractFile archiveFile, String archiveFilePath) throws TskCoreException, NoCurrentCaseException {
371  //check if already has derived files, skip
372  //check if local unpacked dir exists
373  if (archiveFile.hasChildren() && new File(moduleDirAbsolute, EmbeddedFileExtractorIngestModule.getUniqueName(archiveFile)).exists()) {
374  return Case.getCurrentCaseThrows().getServices().getFileManager().findFilesByParentPath(getRootArchiveId(archiveFile), archiveFilePath);
375  }
376  return new ArrayList<>();
377  }
378 
386  private String getArchiveFilePath(AbstractFile archiveFile) {
387  return archiveFile.getParentPath() + archiveFile.getName();
388  }
389 
396  private void makeLocalDirectories(String uniqueArchiveFileName) {
397  final String localRootAbsPath = getLocalRootAbsPath(uniqueArchiveFileName);
398  final File localRoot = new File(localRootAbsPath);
399  if (!localRoot.exists()) {
400  localRoot.mkdirs();
401  }
402  }
403 
415  private String getPathInArchive(IInArchive archive, int inArchiveItemIndex, AbstractFile archiveFile) throws SevenZipException {
416  String pathInArchive = (String) archive.getProperty(inArchiveItemIndex, PropID.PATH);
417 
418  if (pathInArchive == null || pathInArchive.isEmpty()) {
419  //some formats (.tar.gz) may not be handled correctly -- file in archive has no name/path
420  //handle this for .tar.gz and tgz but assuming the child is tar,
421  //otherwise, unpack using itemNumber as name
422 
423  //TODO this should really be signature based, not extension based
424  String archName = archiveFile.getName();
425  int dotI = archName.lastIndexOf(".");
426  String useName = null;
427  if (dotI != -1) {
428  String base = archName.substring(0, dotI);
429  String ext = archName.substring(dotI);
430  int colonIndex = ext.lastIndexOf(":");
431  if (colonIndex != -1) {
432  // If alternate data stream is found, fix the name
433  // so Windows doesn't choke on the colon character.
434  ext = ext.substring(0, colonIndex);
435  }
436  switch (ext) {
437  case ".gz": //NON-NLS
438  useName = base;
439  break;
440  case ".tgz": //NON-NLS
441  useName = base + ".tar"; //NON-NLS
442  break;
443  case ".bz2": //NON-NLS
444  useName = base;
445  break;
446  }
447  }
448  if (useName == null) {
449  pathInArchive = "/" + archName + "/" + Integer.toString(inArchiveItemIndex);
450  } else {
451  pathInArchive = "/" + useName;
452  }
453  }
454  return pathInArchive;
455  }
456 
457  private byte[] getPathBytesInArchive(IInArchive archive, int inArchiveItemIndex, AbstractFile archiveFile) throws SevenZipException {
458  return (byte[]) archive.getProperty(inArchiveItemIndex, PropID.PATH_BYTES);
459  }
460 
461  /*
462  * Get the String that will represent the key for the hashmap which keeps
463  * track of existing files from an AbstractFile
464  */
465  private String getKeyAbstractFile(AbstractFile fileInDatabase) {
466  return fileInDatabase == null ? null : fileInDatabase.getParentPath() + fileInDatabase.getName();
467  }
468 
469  /*
470  * Get the String that will represent the key for the hashmap which keeps
471  * track of existing files from an unpacked node and the archiveFilePath
472  */
473  private String getKeyFromUnpackedNode(UnpackedTree.UnpackedNode node, String archiveFilePath) {
474  return node == null ? null : archiveFilePath + "/" + node.getFileName();
475  }
476 
484  void unpack(AbstractFile archiveFile, ConcurrentHashMap<Long, Archive> depthMap) {
485  unpack(archiveFile, depthMap, null);
486  }
487 
499  @Messages({"SevenZipExtractor.indexError.message=Failed to index encryption detected artifact for keyword search.",
500  "# {0} - rootArchive",
501  "SevenZipExtractor.zipBombArtifactCreation.text=Zip Bomb Detected {0}"})
502  boolean unpack(AbstractFile archiveFile, ConcurrentHashMap<Long, Archive> depthMap, String password) {
503  boolean unpackSuccessful = true; //initialized to true change to false if any files fail to extract and
504  boolean hasEncrypted = false;
505  boolean fullEncryption = true;
506  boolean progressStarted = false;
507  final String archiveFilePath = getArchiveFilePath(archiveFile);
508  final String escapedArchiveFilePath = FileUtil.escapeFileName(archiveFilePath);
509  HashMap<String, ZipFileStatusWrapper> statusMap = new HashMap<>();
510  List<AbstractFile> unpackedFiles = Collections.<AbstractFile>emptyList();
511 
512  currentArchiveName = archiveFile.getName();
513 
514  SevenZipContentReadStream stream = null;
515  progress = ProgressHandle.createHandle(Bundle.EmbeddedFileExtractorIngestModule_ArchiveExtractor_moduleName());
516  //recursion depth check for zip bomb
517  Archive parentAr;
518  try {
519  blackboard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard();
520  } catch (NoCurrentCaseException ex) {
521  logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS
522  unpackSuccessful = false;
523  return unpackSuccessful;
524  }
525  try {
526  List<AbstractFile> existingFiles = getAlreadyExtractedFiles(archiveFile, archiveFilePath);
527  for (AbstractFile file : existingFiles) {
528  statusMap.put(getKeyAbstractFile(file), new ZipFileStatusWrapper(file, ZipFileStatus.EXISTS));
529  }
530  } catch (TskCoreException e) {
531  logger.log(Level.INFO, "Error checking if file already has been processed, skipping: {0}", escapedArchiveFilePath); //NON-NLS
532  unpackSuccessful = false;
533  return unpackSuccessful;
534  } catch (NoCurrentCaseException ex) {
535  logger.log(Level.INFO, "No open case was found while trying to unpack the archive file {0}", escapedArchiveFilePath); //NON-NLS
536  unpackSuccessful = false;
537  return unpackSuccessful;
538  }
539  parentAr = depthMap.get(archiveFile.getId());
540  if (parentAr == null) {
541  parentAr = new Archive(0, archiveFile.getId(), archiveFile);
542  depthMap.put(archiveFile.getId(), parentAr);
543  } else {
544  Archive rootArchive = depthMap.get(parentAr.getRootArchiveId());
545  if (rootArchive.isFlaggedAsZipBomb()) {
546  //skip this archive as the root archive has already been determined to contain a zip bomb
547  unpackSuccessful = false;
548  return unpackSuccessful;
549  } else if (parentAr.getDepth() == MAX_DEPTH) {
550  String details = NbBundle.getMessage(SevenZipExtractor.class,
551  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.warnDetails.zipBomb",
552  parentAr.getDepth(), FileUtil.escapeFileName(getArchiveFilePath(rootArchive.getArchiveFile())));
553  flagRootArchiveAsZipBomb(rootArchive, archiveFile, details, escapedArchiveFilePath);
554  unpackSuccessful = false;
555  return unpackSuccessful;
556  }
557  }
558  IInArchive inArchive = null;
559  try {
560  stream = new SevenZipContentReadStream(new ReadContentInputStream(archiveFile));
561  // for RAR files we need to open them explicitly as RAR. Otherwise, if there is a ZIP archive inside RAR archive
562  // it will be opened incorrectly when using 7zip's built-in auto-detect functionality.
563  // All other archive formats are still opened using 7zip built-in auto-detect functionality.
564  ArchiveFormat options = get7ZipOptions(archiveFile);
565  if (password == null) {
566  inArchive = SevenZip.openInArchive(options, stream);
567  } else {
568  inArchive = SevenZip.openInArchive(options, stream, password);
569  }
570  numItems = inArchive.getNumberOfItems();
571  progress.start(numItems);
572  progressStarted = true;
573 
574  //setup the archive local root folder
575  final String uniqueArchiveFileName = FileUtil.escapeFileName(EmbeddedFileExtractorIngestModule.getUniqueName(archiveFile));
576  try {
577  makeLocalDirectories(uniqueArchiveFileName);
578  } catch (SecurityException e) {
579  logger.log(Level.SEVERE, "Error setting up output path for archive root: {0}", getLocalRootAbsPath(uniqueArchiveFileName)); //NON-NLS
580  //bail
581  unpackSuccessful = false;
582  return unpackSuccessful;
583  }
584 
585  //initialize tree hierarchy to keep track of unpacked file structure
586  SevenZipExtractor.UnpackedTree unpackedTree = new SevenZipExtractor.UnpackedTree(moduleDirRelative + "/" + uniqueArchiveFileName, archiveFile);
587 
588  long freeDiskSpace;
589  try {
590  freeDiskSpace = services.getFreeDiskSpace();
591  } catch (NullPointerException ex) {
592  //If ingest has not been run at least once getFreeDiskSpace() will throw a null pointer exception
593  //currently getFreeDiskSpace always returns DISK_FREE_SPACE_UNKNOWN
594  freeDiskSpace = IngestMonitor.DISK_FREE_SPACE_UNKNOWN;
595  }
596 
597  Map<Integer, InArchiveItemDetails> archiveDetailsMap = new HashMap<>();
598  for (int inArchiveItemIndex = 0; inArchiveItemIndex < numItems; inArchiveItemIndex++) {
599  progress.progress(String.format("%s: Analyzing archive metadata and creating local files (%d of %d)", currentArchiveName, inArchiveItemIndex + 1, numItems), 0);
600  if (isZipBombArchiveItemCheck(archiveFile, inArchive, inArchiveItemIndex, depthMap, escapedArchiveFilePath)) {
601  unpackSuccessful = false;
602  return unpackSuccessful;
603  }
604 
605  String pathInArchive = getPathInArchive(inArchive, inArchiveItemIndex, archiveFile);
606  byte[] pathBytesInArchive = getPathBytesInArchive(inArchive, inArchiveItemIndex, archiveFile);
607  UnpackedTree.UnpackedNode unpackedNode = unpackedTree.addNode(pathInArchive, pathBytesInArchive);
608 
609  final boolean isEncrypted = (Boolean) inArchive.getProperty(inArchiveItemIndex, PropID.ENCRYPTED);
610 
611  if (isEncrypted && password == null) {
612  logger.log(Level.WARNING, "Skipping encrypted file in archive: {0}", pathInArchive); //NON-NLS
613  hasEncrypted = true;
614  unpackSuccessful = false;
615  continue;
616  } else {
617  fullEncryption = false;
618  }
619 
620  // NOTE: item size may return null in case of certain
621  // archiving formats. Eg: BZ2
622  //check if unpacking this file will result in out of disk space
623  //this is additional to zip bomb prevention mechanism
624  Long archiveItemSize = (Long) inArchive.getProperty(
625  inArchiveItemIndex, PropID.SIZE);
626  if (freeDiskSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN && archiveItemSize != null && archiveItemSize > 0) { //if free space is known and file is not empty.
627  String archiveItemPath = (String) inArchive.getProperty(
628  inArchiveItemIndex, PropID.PATH);
629  long newDiskSpace = freeDiskSpace - archiveItemSize;
630  if (newDiskSpace < MIN_FREE_DISK_SPACE) {
631  String msg = NbBundle.getMessage(SevenZipExtractor.class,
632  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.notEnoughDiskSpace.msg",
633  escapedArchiveFilePath, archiveItemPath);
634  String details = NbBundle.getMessage(SevenZipExtractor.class,
635  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.notEnoughDiskSpace.details");
636  services.postMessage(IngestMessage.createErrorMessage(MODULE_NAME, msg, details));
637  logger.log(Level.INFO, "Skipping archive item due to insufficient disk space: {0}, {1}", new String[]{escapedArchiveFilePath, archiveItemPath}); //NON-NLS
638  logger.log(Level.INFO, "Available disk space: {0}", new Object[]{freeDiskSpace}); //NON-NLS
639  unpackSuccessful = false;
640  continue; //skip this file
641  } else {
642  //update est. disk space during this archive, so we don't need to poll for every file extracted
643  freeDiskSpace = newDiskSpace;
644  }
645  }
646  final String uniqueExtractedName = FileUtil.escapeFileName(uniqueArchiveFileName + File.separator + (inArchiveItemIndex / 1000) + File.separator + inArchiveItemIndex + "_" + new File(pathInArchive).getName());
647  final String localAbsPath = moduleDirAbsolute + File.separator + uniqueExtractedName;
648  final String localRelPath = moduleDirRelative + File.separator + uniqueExtractedName;
649 
650  //create local dirs and empty files before extracted
651  File localFile = new java.io.File(localAbsPath);
652  //cannot rely on files in top-bottom order
653  if (!localFile.exists()) {
654  try {
655  if ((Boolean) inArchive.getProperty(
656  inArchiveItemIndex, PropID.IS_FOLDER)) {
657  localFile.mkdirs();
658  } else {
659  localFile.getParentFile().mkdirs();
660  try {
661  localFile.createNewFile();
662  } catch (IOException e) {
663  logger.log(Level.SEVERE, "Error creating extracted file: "//NON-NLS
664  + localFile.getAbsolutePath(), e);
665  }
666  }
667  } catch (SecurityException e) {
668  logger.log(Level.SEVERE, "Error setting up output path for unpacked file: {0}", //NON-NLS
669  pathInArchive); //NON-NLS
670  //TODO consider bail out / msg to the user
671  }
672  }
673  // skip the rest of this loop if we couldn't create the file
674  //continue will skip details from being added to the map
675  if (localFile.exists() == false) {
676  continue;
677  }
678 
679  //Store archiveItemIndex with local paths and unpackedNode reference.
680  //Necessary for the extract call back to write the current archive
681  //file to the correct disk location and to correctly update it's
682  //corresponding unpackedNode
683  archiveDetailsMap.put(inArchiveItemIndex, new InArchiveItemDetails(
684  unpackedNode, localAbsPath, localRelPath));
685  }
686 
687  int[] extractionIndices = getExtractableFilesFromDetailsMap(archiveDetailsMap);
688 
689  StandardIArchiveExtractCallback archiveCallBack
690  = new StandardIArchiveExtractCallback(
691  inArchive, archiveFile, progress,
692  archiveDetailsMap, password, freeDiskSpace);
693 
694  //According to the documentation, indices in sorted order are optimal
695  //for efficiency. Hence, the HashMap and linear processing of
696  //inArchiveItemIndex. False indicates non-test mode
697  inArchive.extract(extractionIndices, false, archiveCallBack);
698 
699  unpackSuccessful &= archiveCallBack.wasSuccessful();
700 
701  archiveDetailsMap = null;
702 
703  // add them to the DB. We wait until the end so that we have the metadata on all of the
704  // intermediate nodes since the order is not guaranteed
705  try {
706  unpackedTree.updateOrAddFileToCaseRec(statusMap, archiveFilePath);
707  unpackedFiles = unpackedTree.getAllFileObjects();
708  //check if children are archives, update archive depth tracking
709  for (int i = 0; i < unpackedFiles.size(); i++) {
710  progress.progress(String.format("%s: Searching for nested archives (%d of %d)", currentArchiveName, i + 1, unpackedFiles.size()));
711  AbstractFile unpackedFile = unpackedFiles.get(i);
712  if (unpackedFile == null) {
713  continue;
714  }
715  if (isSevenZipExtractionSupported(unpackedFile)) {
716  Archive child = new Archive(parentAr.getDepth() + 1, parentAr.getRootArchiveId(), archiveFile);
717  parentAr.addChild(child);
718  depthMap.put(unpackedFile.getId(), child);
719  }
720  unpackedFile.close();
721  }
722 
723  } catch (TskCoreException | NoCurrentCaseException e) {
724  logger.log(Level.SEVERE, "Error populating complete derived file hierarchy from the unpacked dir structure", e); //NON-NLS
725  //TODO decide if anything to cleanup, for now bailing
726  }
727 
728  } catch (SevenZipException ex) {
729  logger.log(Level.WARNING, "Error unpacking file: " + archiveFile, ex); //NON-NLS
730  //inbox message
731 
732  // print a message if the file is allocated
733  if (archiveFile.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
734  String msg = NbBundle.getMessage(SevenZipExtractor.class,
735  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.errUnpacking.msg",
736  currentArchiveName);
737  String details = NbBundle.getMessage(SevenZipExtractor.class,
738  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.errUnpacking.details",
739  escapedArchiveFilePath, ex.getMessage());
740  services.postMessage(IngestMessage.createErrorMessage(MODULE_NAME, msg, details));
741  }
742  } finally {
743  if (inArchive != null) {
744  try {
745  inArchive.close();
746  } catch (SevenZipException e) {
747  logger.log(Level.SEVERE, "Error closing archive: " + archiveFile, e); //NON-NLS
748  }
749  }
750 
751  if (stream != null) {
752  try {
753  stream.close();
754  } catch (IOException ex) {
755  logger.log(Level.SEVERE, "Error closing stream after unpacking archive: " + archiveFile, ex); //NON-NLS
756  }
757  }
758 
759  //close progress bar
760  if (progressStarted) {
761  progress.finish();
762  }
763  }
764 
765  //create artifact and send user message
766  if (hasEncrypted) {
767  String encryptionType = fullEncryption ? ENCRYPTION_FULL : ENCRYPTION_FILE_LEVEL;
768  try {
769  BlackboardArtifact artifact = archiveFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED);
770  artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT, MODULE_NAME, encryptionType));
771 
772  try {
773  /*
774  * post the artifact which will index the artifact for
775  * keyword search, and fire an event to notify UI of this
776  * new artifact
777  */
778  blackboard.postArtifact(artifact, MODULE_NAME);
779  } catch (Blackboard.BlackboardException ex) {
780  logger.log(Level.SEVERE, "Unable to post blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
781  MessageNotifyUtil.Notify.error(
782  Bundle.SevenZipExtractor_indexError_message(), artifact.getDisplayName());
783  }
784 
785  } catch (TskCoreException ex) {
786  logger.log(Level.SEVERE, "Error creating blackboard artifact for encryption detected for file: " + escapedArchiveFilePath, ex); //NON-NLS
787  }
788 
789  String msg = NbBundle.getMessage(SevenZipExtractor.class,
790  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.encrFileDetected.msg");
791  String details = NbBundle.getMessage(SevenZipExtractor.class,
792  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.unpack.encrFileDetected.details",
793  currentArchiveName, MODULE_NAME);
794  services.postMessage(IngestMessage.createWarningMessage(MODULE_NAME, msg, details));
795  }
796 
797  // adding unpacked extracted derived files to the job after closing relevant resources.
798  if (!unpackedFiles.isEmpty()) {
799  //currently sending a single event for all new files
800  services.fireModuleContentEvent(new ModuleContentEvent(archiveFile));
801  if (context != null) {
802  context.addFilesToJob(unpackedFiles);
803  }
804  }
805  return unpackSuccessful;
806  }
807 
808  private Charset detectFilenamesCharset(List<byte[]> byteDatas) {
809  Charset detectedCharset = null;
810  CharsetDetector charsetDetector = new CharsetDetector();
811  int byteSum = 0;
812  int fileNum = 0;
813  for (byte[] byteData : byteDatas) {
814  fileNum++;
815  byteSum += byteData.length;
816  // Only read ~1000 bytes of filenames in this directory
817  if (byteSum >= 1000) {
818  break;
819  }
820  }
821  byte[] allBytes = new byte[byteSum];
822  int start = 0;
823  for (int i = 0; i < fileNum; i++) {
824  byte[] byteData = byteDatas.get(i);
825  System.arraycopy(byteData, 0, allBytes, start, byteData.length);
826  start += byteData.length;
827  }
828  charsetDetector.setText(allBytes);
829  CharsetMatch cm = charsetDetector.detect();
830  if (cm.getConfidence() >= 90 && Charset.isSupported(cm.getName())) {
831  detectedCharset = Charset.forName(cm.getName());
832  }
833  return detectedCharset;
834  }
835 
840  private int[] getExtractableFilesFromDetailsMap(
841  Map<Integer, InArchiveItemDetails> archiveDetailsMap) {
842 
843  Integer[] wrappedExtractionIndices = archiveDetailsMap.keySet()
844  .toArray(new Integer[archiveDetailsMap.size()]);
845 
846  return Arrays.stream(wrappedExtractionIndices)
847  .mapToInt(Integer::intValue)
848  .toArray();
849  }
850 
858  private final static class UnpackStream implements ISequentialOutStream {
859 
860  private EncodedFileOutputStream output;
861  private String localAbsPath;
862  private int bytesWritten;
863 
864  UnpackStream(String localAbsPath) throws IOException {
865  this.output = new EncodedFileOutputStream(new FileOutputStream(localAbsPath), TskData.EncodingType.XOR1);
866  this.localAbsPath = localAbsPath;
867  this.bytesWritten = 0;
868  }
869 
870  public void setNewOutputStream(String localAbsPath) throws IOException {
871  this.output.close();
872  this.output = new EncodedFileOutputStream(new FileOutputStream(localAbsPath), TskData.EncodingType.XOR1);
873  this.localAbsPath = localAbsPath;
874  this.bytesWritten = 0;
875  }
876 
877  public int getSize() {
878  return bytesWritten;
879  }
880 
881  @Override
882  public int write(byte[] bytes) throws SevenZipException {
883  try {
884  output.write(bytes);
885  this.bytesWritten += bytes.length;
886  } catch (IOException ex) {
887  throw new SevenZipException(
888  NbBundle.getMessage(SevenZipExtractor.class,
889  "EmbeddedFileExtractorIngestModule.ArchiveExtractor.UnpackStream.write.exception.msg",
890  localAbsPath), ex);
891  }
892  return bytes.length;
893  }
894 
895  public void close() throws IOException {
896  try (EncodedFileOutputStream out = output) {
897  out.flush();
898  }
899  }
900 
901  }
902 
906  private static class InArchiveItemDetails {
907 
908  private final SevenZipExtractor.UnpackedTree.UnpackedNode unpackedNode;
909  private final String localAbsPath;
910  private final String localRelPath;
911 
913  SevenZipExtractor.UnpackedTree.UnpackedNode unpackedNode,
914  String localAbsPath, String localRelPath) {
915  this.unpackedNode = unpackedNode;
916  this.localAbsPath = localAbsPath;
917  this.localRelPath = localRelPath;
918  }
919 
920  public SevenZipExtractor.UnpackedTree.UnpackedNode getUnpackedNode() {
921  return unpackedNode;
922  }
923 
924  public String getLocalAbsPath() {
925  return localAbsPath;
926  }
927 
928  public String getLocalRelPath() {
929  return localRelPath;
930  }
931  }
932 
937  private static class StandardIArchiveExtractCallback
938  implements IArchiveExtractCallback, ICryptoGetTextPassword {
939 
940  private final AbstractFile archiveFile;
941  private final IInArchive inArchive;
942  private UnpackStream unpackStream = null;
943  private final Map<Integer, InArchiveItemDetails> archiveDetailsMap;
944  private final ProgressHandle progressHandle;
945 
946  private int inArchiveItemIndex;
947 
948  private long createTimeInSeconds;
949  private long modTimeInSeconds;
950  private long accessTimeInSeconds;
951 
952  private boolean isFolder;
953  private final String password;
954 
955  private boolean unpackSuccessful = true;
956 
957  StandardIArchiveExtractCallback(IInArchive inArchive,
958  AbstractFile archiveFile, ProgressHandle progressHandle,
959  Map<Integer, InArchiveItemDetails> archiveDetailsMap,
960  String password, long freeDiskSpace) {
961  this.inArchive = inArchive;
962  this.progressHandle = progressHandle;
963  this.archiveFile = archiveFile;
964  this.archiveDetailsMap = archiveDetailsMap;
965  this.password = password;
966  }
967 
982  @Override
983  public ISequentialOutStream getStream(int inArchiveItemIndex,
984  ExtractAskMode mode) throws SevenZipException {
985 
986  this.inArchiveItemIndex = inArchiveItemIndex;
987 
988  isFolder = (Boolean) inArchive
989  .getProperty(inArchiveItemIndex, PropID.IS_FOLDER);
990  if (isFolder || mode != ExtractAskMode.EXTRACT) {
991  return null;
992  }
993 
994  final String localAbsPath = archiveDetailsMap.get(
995  inArchiveItemIndex).getLocalAbsPath();
996 
997  //If the Unpackstream has been allocated, then set the Outputstream
998  //to another file rather than creating a new unpack stream. The 7Zip
999  //binding has a memory leak, so creating new unpack streams will not be
1000  //dereferenced. As a fix, we create one UnpackStream, and mutate its state,
1001  //so that there only exists one 8192 byte buffer in memory per archive.
1002  try {
1003  if (unpackStream != null) {
1004  unpackStream.setNewOutputStream(localAbsPath);
1005  } else {
1006  unpackStream = new UnpackStream(localAbsPath);
1007  }
1008  } catch (IOException ex) {
1009  logger.log(Level.WARNING, String.format("Error opening or setting new stream " //NON-NLS
1010  + "for archive file at %s", localAbsPath), ex.getMessage()); //NON-NLS
1011  return null;
1012  }
1013 
1014  return unpackStream;
1015  }
1016 
1025  @Override
1026  public void prepareOperation(ExtractAskMode mode) throws SevenZipException {
1027  final Date createTime = (Date) inArchive.getProperty(
1028  inArchiveItemIndex, PropID.CREATION_TIME);
1029  final Date accessTime = (Date) inArchive.getProperty(
1030  inArchiveItemIndex, PropID.LAST_ACCESS_TIME);
1031  final Date writeTime = (Date) inArchive.getProperty(
1032  inArchiveItemIndex, PropID.LAST_MODIFICATION_TIME);
1033 
1034  createTimeInSeconds = createTime == null ? 0L
1035  : createTime.getTime() / 1000;
1036  modTimeInSeconds = writeTime == null ? 0L
1037  : writeTime.getTime() / 1000;
1038  accessTimeInSeconds = accessTime == null ? 0L
1039  : accessTime.getTime() / 1000;
1040 
1041  progressHandle.progress(archiveFile.getName() + ": "
1042  + (String) inArchive.getProperty(inArchiveItemIndex, PropID.PATH),
1044 
1045  }
1046 
1055  @Override
1056  public void setOperationResult(ExtractOperationResult result) throws SevenZipException {
1057 
1058  final SevenZipExtractor.UnpackedTree.UnpackedNode unpackedNode
1059  = archiveDetailsMap.get(inArchiveItemIndex).getUnpackedNode();
1060  final String localRelPath = archiveDetailsMap.get(
1061  inArchiveItemIndex).getLocalRelPath();
1062  if (isFolder) {
1063  unpackedNode.addDerivedInfo(0,
1064  !(Boolean) inArchive.getProperty(inArchiveItemIndex, PropID.IS_FOLDER),
1066  localRelPath);
1067  return;
1068  }
1069 
1070  final String localAbsPath = archiveDetailsMap.get(
1071  inArchiveItemIndex).getLocalAbsPath();
1072  if (result != ExtractOperationResult.OK) {
1073  logger.log(Level.WARNING, "Extraction of : {0} encountered error {1}", //NON-NLS
1074  new Object[]{localAbsPath, result});
1075  unpackSuccessful = false;
1076  }
1077 
1078  //record derived data in unode, to be traversed later after unpacking the archive
1079  unpackedNode.addDerivedInfo(unpackStream.getSize(),
1080  !(Boolean) inArchive.getProperty(inArchiveItemIndex, PropID.IS_FOLDER),
1082 
1083  try {
1084  unpackStream.close();
1085  } catch (IOException e) {
1086  logger.log(Level.WARNING, "Error closing unpack stream for file: {0}", localAbsPath); //NON-NLS
1087  }
1088  }
1089 
1090  @Override
1091  public void setTotal(long value) throws SevenZipException {
1092  //Not necessary for extract, left intenionally blank
1093  }
1094 
1095  @Override
1096  public void setCompleted(long value) throws SevenZipException {
1097  //Not necessary for extract, left intenionally blank
1098  }
1099 
1107  @Override
1108  public String cryptoGetTextPassword() throws SevenZipException {
1109  return password;
1110  }
1111 
1112  public boolean wasSuccessful() {
1113  return unpackSuccessful;
1114  }
1115  }
1116 
1124  private class UnpackedTree {
1125 
1126  final UnpackedNode rootNode;
1127  private int nodesProcessed = 0;
1128 
1135  UnpackedTree(String localPathRoot, AbstractFile archiveFile) {
1136  this.rootNode = new UnpackedNode();
1137  this.rootNode.setFile(archiveFile);
1138  this.rootNode.setFileName(archiveFile.getName());
1139  this.rootNode.setLocalRelPath(localPathRoot);
1140  }
1141 
1151  UnpackedNode addNode(String filePath, byte[] filePathBytes) {
1152  String[] toks = filePath.split("[\\/\\\\]");
1153  List<String> tokens = new ArrayList<>();
1154  for (int i = 0; i < toks.length; ++i) {
1155  if (!toks[i].isEmpty()) {
1156  tokens.add(toks[i]);
1157  }
1158  }
1159 
1160  List<byte[]> byteTokens = null;
1161  if (filePathBytes == null) {
1162  return addNode(rootNode, tokens, null);
1163  } else {
1164  byteTokens = new ArrayList<>(tokens.size());
1165  int last = 0;
1166  for (int i = 0; i < filePathBytes.length; i++) {
1167  if (filePathBytes[i] == '/') {
1168  int len = i - last;
1169  byte[] arr = new byte[len];
1170  System.arraycopy(filePathBytes, last, arr, 0, len);
1171  byteTokens.add(arr);
1172  last = i + 1;
1173  }
1174  }
1175  int len = filePathBytes.length - last;
1176  if (len > 0) {
1177  byte[] arr = new byte[len];
1178  System.arraycopy(filePathBytes, last, arr, 0, len);
1179  byteTokens.add(arr);
1180  }
1181 
1182  if (tokens.size() != byteTokens.size()) {
1183  logger.log(Level.WARNING, "Could not map path bytes to path string");
1184  return addNode(rootNode, tokens, null);
1185  }
1186  }
1187 
1188  return addNode(rootNode, tokens, byteTokens);
1189  }
1190 
1201  List<String> tokenPath, List<byte[]> tokenPathBytes) {
1202  // we found all of the tokens
1203  if (tokenPath.isEmpty()) {
1204  return parent;
1205  }
1206 
1207  // get the next name in the path and look it up
1208  String childName = tokenPath.remove(0);
1209  byte[] childNameBytes = null;
1210  if (tokenPathBytes != null) {
1211  childNameBytes = tokenPathBytes.remove(0);
1212  }
1213  UnpackedNode child = parent.getChild(childName);
1214  // create new node
1215  if (child == null) {
1216  child = new UnpackedNode(childName, parent);
1217  child.setFileNameBytes(childNameBytes);
1218  parent.addChild(child);
1219  }
1220 
1221  // go down one more level
1222  return addNode(child, tokenPath, tokenPathBytes);
1223  }
1224 
1231  List<AbstractFile> getRootFileObjects() {
1232  List<AbstractFile> ret = new ArrayList<>();
1233  rootNode.getChildren().forEach((child) -> {
1234  ret.add(child.getFile());
1235  });
1236  return ret;
1237  }
1238 
1245  List<AbstractFile> getAllFileObjects() {
1246  List<AbstractFile> ret = new ArrayList<>();
1247  rootNode.getChildren().forEach((child) -> {
1248  getAllFileObjectsRec(ret, child);
1249  });
1250  return ret;
1251  }
1252 
1253  private void getAllFileObjectsRec(List<AbstractFile> list, UnpackedNode parent) {
1254  list.add(parent.getFile());
1255  parent.getChildren().forEach((child) -> {
1256  getAllFileObjectsRec(list, child);
1257  });
1258  }
1259 
1264  void updateOrAddFileToCaseRec(HashMap<String, ZipFileStatusWrapper> statusMap, String archiveFilePath) throws TskCoreException, NoCurrentCaseException {
1266  for (UnpackedNode child : rootNode.getChildren()) {
1267  updateOrAddFileToCaseRec(child, fileManager, statusMap, archiveFilePath);
1268  }
1269  }
1270 
1285  private void updateOrAddFileToCaseRec(UnpackedNode node, FileManager fileManager, HashMap<String, ZipFileStatusWrapper> statusMap, String archiveFilePath) throws TskCoreException {
1286  DerivedFile df;
1287  progress.progress(String.format("%s: Adding/updating files in case database (%d of %d)", currentArchiveName, ++nodesProcessed, numItems));
1288  try {
1289  String nameInDatabase = getKeyFromUnpackedNode(node, archiveFilePath);
1290  ZipFileStatusWrapper existingFile = nameInDatabase == null ? null : statusMap.get(nameInDatabase);
1291  if (existingFile == null) {
1292  df = fileManager.addDerivedFile(node.getFileName(), node.getLocalRelPath(), node.getSize(),
1293  node.getCtime(), node.getCrtime(), node.getAtime(), node.getMtime(),
1294  node.isIsFile(), node.getParent().getFile(), "", MODULE_NAME,
1295  "", "", TskData.EncodingType.XOR1);
1296  statusMap.put(getKeyAbstractFile(df), new ZipFileStatusWrapper(df, ZipFileStatus.EXISTS));
1297  } else {
1298  String key = getKeyAbstractFile(existingFile.getFile());
1299  if (existingFile.getStatus() == ZipFileStatus.EXISTS && existingFile.getFile().getSize() < node.getSize()) {
1300  existingFile.setStatus(ZipFileStatus.UPDATE);
1301  statusMap.put(key, existingFile);
1302  }
1303  if (existingFile.getStatus() == ZipFileStatus.UPDATE) {
1304  //if the we are updating a file and its mime type was octet-stream we want to re-type it
1305  String mimeType = existingFile.getFile().getMIMEType().equalsIgnoreCase("application/octet-stream") ? null : existingFile.getFile().getMIMEType();
1306  df = fileManager.updateDerivedFile((DerivedFile) existingFile.getFile(), node.getLocalRelPath(), node.getSize(),
1307  node.getCtime(), node.getCrtime(), node.getAtime(), node.getMtime(),
1308  node.isIsFile(), mimeType, "", MODULE_NAME,
1309  "", "", TskData.EncodingType.XOR1);
1310  } else {
1311  //ALREADY CURRENT - SKIP
1312  statusMap.put(key, new ZipFileStatusWrapper(existingFile.getFile(), ZipFileStatus.SKIP));
1313  df = (DerivedFile) existingFile.getFile();
1314  }
1315  }
1316  node.setFile(df);
1317  } catch (TskCoreException ex) {
1318  logger.log(Level.SEVERE, "Error adding a derived file to db:" + node.getFileName(), ex); //NON-NLS
1319  throw new TskCoreException(
1320  NbBundle.getMessage(SevenZipExtractor.class, "EmbeddedFileExtractorIngestModule.ArchiveExtractor.UnpackedTree.exception.msg",
1321  node.getFileName()), ex);
1322  }
1323 
1324  // Determine encoding of children
1325  if (node.getChildren().size() > 0) {
1326  String names = "";
1327  ArrayList<byte[]> byteDatas = new ArrayList<>();
1328  for (UnpackedNode child : node.getChildren()) {
1329  byte[] childBytes = child.getFileNameBytes();
1330  if (childBytes != null) {
1331  byteDatas.add(childBytes);
1332  }
1333  names += child.getFileName();
1334  }
1335  Charset detectedCharset = detectFilenamesCharset(byteDatas);
1336 
1337  // If a charset was detected, transcode filenames accordingly
1338  if (detectedCharset != null && detectedCharset.canEncode()) {
1339  for (UnpackedNode child : node.getChildren()) {
1340  byte[] childBytes = child.getFileNameBytes();
1341  if (childBytes != null) {
1342  String decodedName = new String(childBytes, detectedCharset);
1343  child.setFileName(decodedName);
1344  }
1345  }
1346  }
1347  }
1348 
1349  //recurse adding the children if this file was incomplete the children presumably need to be added
1350  for (UnpackedNode child : node.getChildren()) {
1351  updateOrAddFileToCaseRec(child, fileManager, statusMap, getKeyFromUnpackedNode(node, archiveFilePath));
1352  }
1353  }
1354 
1358  private class UnpackedNode {
1359 
1360  private String fileName;
1361  private byte[] fileNameBytes;
1362  private AbstractFile file;
1363  private final List<UnpackedNode> children = new ArrayList<>();
1364  private String localRelPath = "";
1365  private long size;
1366  private long ctime, crtime, atime, mtime;
1367  private boolean isFile;
1369 
1370  //root constructor
1371  UnpackedNode() {
1372  }
1373 
1374  //child node constructor
1375  UnpackedNode(String fileName, UnpackedNode parent) {
1376  this.fileName = fileName;
1377  this.parent = parent;
1378  this.localRelPath = parent.getLocalRelPath() + File.separator + fileName;
1379  }
1380 
1381  long getCtime() {
1382  return ctime;
1383  }
1384 
1385  long getCrtime() {
1386  return crtime;
1387  }
1388 
1389  long getAtime() {
1390  return atime;
1391  }
1392 
1393  long getMtime() {
1394  return mtime;
1395  }
1396 
1397  void setFileName(String fileName) {
1398  this.fileName = fileName;
1399  }
1400 
1406  void addChild(UnpackedNode child) {
1407  children.add(child);
1408  }
1409 
1416  List<UnpackedNode> getChildren() {
1417  return children;
1418  }
1419 
1425  UnpackedNode getParent() {
1426  return parent;
1427  }
1428 
1429  void addDerivedInfo(long size,
1430  boolean isFile,
1431  long ctime, long crtime, long atime, long mtime, String relLocalPath) {
1432  this.size = size;
1433  this.isFile = isFile;
1434  this.ctime = ctime;
1435  this.crtime = crtime;
1436  this.atime = atime;
1437  this.mtime = mtime;
1438  this.localRelPath = relLocalPath;
1439  }
1440 
1441  void setFile(AbstractFile file) {
1442  this.file = file;
1443  }
1444 
1452  UnpackedNode getChild(String childFileName) {
1453  UnpackedNode ret = null;
1454  for (UnpackedNode child : children) {
1455  if (child.getFileName().equals(childFileName)) {
1456  ret = child;
1457  break;
1458  }
1459  }
1460  return ret;
1461  }
1462 
1463  String getFileName() {
1464  return fileName;
1465  }
1466 
1467  AbstractFile getFile() {
1468  return file;
1469  }
1470 
1471  String getLocalRelPath() {
1472  return localRelPath;
1473  }
1474 
1481  void setLocalRelPath(String localRelativePath) {
1482  localRelPath = localRelativePath;
1483  }
1484 
1485  long getSize() {
1486  return size;
1487  }
1488 
1489  boolean isIsFile() {
1490  return isFile;
1491  }
1492 
1493  void setFileNameBytes(byte[] fileNameBytes) {
1494  if (fileNameBytes != null) {
1495  this.fileNameBytes = Arrays.copyOf(fileNameBytes, fileNameBytes.length);
1496  }
1497  }
1498 
1499  byte[] getFileNameBytes() {
1500  if (fileNameBytes == null) {
1501  return null;
1502  }
1503  return Arrays.copyOf(fileNameBytes, fileNameBytes.length);
1504  }
1505  }
1506  }
1507 
1512  static class Archive {
1513 
1514  //depth will be 0 for the root archive unpack was called on, and increase as unpack recurses down through archives contained within
1515  private final int depth;
1516  private final List<Archive> children;
1517  private final long rootArchiveId;
1518  private boolean flaggedAsZipBomb = false;
1519  private final AbstractFile archiveFile;
1520 
1533  Archive(int depth, long rootArchiveId, AbstractFile archiveFile) {
1534  this.children = new ArrayList<>();
1535  this.depth = depth;
1536  this.rootArchiveId = rootArchiveId;
1537  this.archiveFile = archiveFile;
1538  }
1539 
1546  void addChild(Archive child) {
1547  children.add(child);
1548  }
1549 
1554  synchronized void flagAsZipBomb() {
1555  flaggedAsZipBomb = true;
1556  }
1557 
1563  synchronized boolean isFlaggedAsZipBomb() {
1564  return flaggedAsZipBomb;
1565  }
1566 
1572  AbstractFile getArchiveFile() {
1573  return archiveFile;
1574  }
1575 
1581  long getRootArchiveId() {
1582  return rootArchiveId;
1583  }
1584 
1590  long getObjectId() {
1591  return archiveFile.getId();
1592  }
1593 
1601  int getDepth() {
1602  return depth;
1603  }
1604  }
1605 
1610  private final class ZipFileStatusWrapper {
1611 
1612  private final AbstractFile abstractFile;
1614 
1622  private ZipFileStatusWrapper(AbstractFile file, ZipFileStatus status) {
1623  abstractFile = file;
1624  zipStatus = status;
1625  }
1626 
1632  private AbstractFile getFile() {
1633  return abstractFile;
1634  }
1635 
1642  return zipStatus;
1643  }
1644 
1650  private void setStatus(ZipFileStatus status) {
1651  zipStatus = status;
1652  }
1653 
1654  }
1655 
1660  private enum ZipFileStatus {
1661  UPDATE, //Should be updated //NON-NLS
1662  SKIP, //File is current can be skipped //NON-NLS
1663  EXISTS //File exists but it is unknown if it is current //NON-NLS
1664  }
1665 }
void updateOrAddFileToCaseRec(UnpackedNode node, FileManager fileManager, HashMap< String, ZipFileStatusWrapper > statusMap, String archiveFilePath)
synchronized DerivedFile addDerivedFile(String fileName, String localPath, long size, long ctime, long crtime, long atime, long mtime, boolean isFile, Content parentObj, String rederiveDetails, String toolName, String toolVersion, String otherDetails, TskData.EncodingType encodingType)
UnpackedNode addNode(UnpackedNode parent, List< String > tokenPath, List< byte[]> tokenPathBytes)
synchronized DerivedFile updateDerivedFile(DerivedFile derivedFile, String localPath, long size, long ctime, long crtime, long atime, long mtime, boolean isFile, String mimeType, String rederiveDetails, String toolName, String toolVersion, String otherDetails, TskData.EncodingType encodingType)

Copyright © 2012-2020 Basis Technology. Generated on: Tue Sep 22 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.