Autopsy  4.16.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
HEICProcessor.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2020 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.pictureanalyzer.impls;
20 
21 import java.util.HashSet;
22 import java.util.Set;
23 import java.util.logging.Level;
24 import java.util.Arrays;
25 import java.util.concurrent.TimeUnit;
26 
27 import java.io.BufferedInputStream;
28 import java.io.File;
29 import java.io.IOException;
30 
31 import java.nio.file.DirectoryIteratorException;
32 import java.nio.file.DirectoryStream;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.nio.file.StandardCopyOption;
37 import java.nio.file.attribute.BasicFileAttributes;
38 
39 import org.apache.commons.io.FilenameUtils;
40 
41 import org.openide.modules.InstalledFileLocator;
42 import org.openide.util.lookup.ServiceProvider;
43 
54 
55 import org.sleuthkit.datamodel.AbstractFile;
56 import org.sleuthkit.datamodel.DerivedFile;
57 import org.sleuthkit.datamodel.ReadContentInputStream;
58 import org.sleuthkit.datamodel.TskCoreException;
59 import org.sleuthkit.datamodel.TskData;
60 
67 @ServiceProvider(service = PictureProcessor.class)
68 public class HEICProcessor implements PictureProcessor {
69 
70  private static final Logger logger = Logger.getLogger(HEICProcessor.class.getName());
71 
72  private static final int EXIT_SUCCESS = 0;
73  private static final String HEIC_MODULE_FOLDER = "HEIC";
74  private static final long TIMEOUT_IN_MS = TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES);
75 
76  // Windows location
77  private static final String IMAGE_MAGICK_FOLDER = "ImageMagick-7.0.10-27-portable-Q16-x64";
78  private static final String IMAGE_MAGICK_EXE = "magick.exe";
79  private static final String IMAGE_MAGICK_ERROR_FILE = "magick_error.txt";
80 
81  // Actual path of ImageMagick on the system
82  private final Path IMAGE_MAGICK_PATH;
83 
84  public HEICProcessor() {
85  IMAGE_MAGICK_PATH = findImageMagick();
86 
87  if (IMAGE_MAGICK_PATH == null) {
88  logger.log(Level.WARNING, "ImageMagick executable not found. "
89  + "HEIC functionality will be automatically disabled.");
90  }
91  }
92 
93  private Path findImageMagick() {
94  final Path windowsLocation = Paths.get(IMAGE_MAGICK_FOLDER, IMAGE_MAGICK_EXE);
95  final Path macAndLinuxLocation = Paths.get("/usr", "local", "bin", "magick");
96 
97  final String osName = PlatformUtil.getOSName().toLowerCase();
98 
100  final File locatedExec = InstalledFileLocator.getDefault().locate(
101  windowsLocation.toString(), HEICProcessor.class.getPackage().getName(), false);
102 
103  return (locatedExec != null) ? locatedExec.toPath() : null;
104  } else if ((osName.equals("linux") || osName.startsWith("mac")) &&
105  Files.isExecutable(macAndLinuxLocation) &&
106  !Files.isDirectory(macAndLinuxLocation)) {
107  return macAndLinuxLocation;
108  } else {
109  return null;
110  }
111  }
112 
117  private Path getModuleOutputFolder(AbstractFile file) throws NoCurrentCaseException {
118  final String moduleOutputDirectory = Case.getCurrentCaseThrows().getModuleDirectory();
119 
120  return Paths.get(moduleOutputDirectory,
121  HEIC_MODULE_FOLDER,
122  String.valueOf(file.getId()));
123  }
124 
128  private void createModuleOutputFolder(AbstractFile file) throws IOException, NoCurrentCaseException {
129  final Path moduleOutputFolder = getModuleOutputFolder(file);
130 
131  if (!Files.exists(moduleOutputFolder)) {
132  Files.createDirectories(moduleOutputFolder);
133  }
134  }
135 
136  @Override
137  public void process(IngestJobContext context, AbstractFile file) {
138  try {
139  if (IMAGE_MAGICK_PATH == null) {
140  return;
141  }
142  createModuleOutputFolder(file);
143 
144  if (context.fileIngestIsCancelled()) {
145  return;
146  }
147 
148  final Path localDiskCopy = extractToDisk(file);
149 
150  convertToJPEG(context, localDiskCopy, file);
151  } catch (IOException ex) {
152  logger.log(Level.WARNING, "I/O error encountered during HEIC photo processing.", ex);
153  } catch (TskCoreException ex) {
154  logger.log(Level.SEVERE, "Unable to add pictures as derived files.", ex);
155  } catch (NoCurrentCaseException ex) {
156  logger.log(Level.WARNING, "No open case!", ex);
157  }
158  }
159 
163  private Path extractToDisk(AbstractFile heicFile) throws IOException, NoCurrentCaseException {
164  final String tempDir = Case.getCurrentCaseThrows().getTempDirectory();
165  final String heicFileName = FileUtil.escapeFileName(heicFile.getName());
166 
167  final Path localDiskCopy = Paths.get(tempDir, heicFileName);
168 
169  try (BufferedInputStream heicInputStream = new BufferedInputStream(new ReadContentInputStream(heicFile))) {
170  Files.copy(heicInputStream, localDiskCopy, StandardCopyOption.REPLACE_EXISTING);
171  return localDiskCopy;
172  }
173  }
174 
175  private void convertToJPEG(IngestJobContext context, Path localDiskCopy,
176  AbstractFile heicFile) throws IOException, TskCoreException, NoCurrentCaseException {
177 
178  // First step, run ImageMagick against this heic container.
179  final Path moduleOutputFolder = getModuleOutputFolder(heicFile);
180 
181  final String baseFileName = FilenameUtils.getBaseName(FileUtil.escapeFileName(heicFile.getName()));
182  final Path outputFile = moduleOutputFolder.resolve(baseFileName + ".jpg");
183 
184  final Path imageMagickErrorOutput = moduleOutputFolder.resolve(IMAGE_MAGICK_ERROR_FILE);
185  Files.createFile(imageMagickErrorOutput);
186 
187  // ImageMagick will write the primary image to the output file.
188  // Any additional images found within the HEIC container will be
189  // formatted as fileName-1.jpg, fileName-2.jpg, etc.
190  final ProcessBuilder processBuilder = new ProcessBuilder()
191  .command(IMAGE_MAGICK_PATH.toString(),
192  localDiskCopy.toString(),
193  outputFile.toString());
194 
195  processBuilder.redirectError(imageMagickErrorOutput.toFile());
196 
197  final long startTime = System.currentTimeMillis();
198  final int exitStatus = ExecUtil.execute(processBuilder, () -> {
199  return context.fileIngestIsCancelled() || System.currentTimeMillis() - startTime >= TIMEOUT_IN_MS;
200  });
201 
202  if (context.fileIngestIsCancelled()) {
203  return;
204  }
205 
206  if (exitStatus != EXIT_SUCCESS) {
207  logger.log(Level.INFO, "Non-zero exit status for HEIC file [id: {0}]. Skipping...", heicFile.getId());
208  return;
209  }
210 
211  // Second step, visit all the output files and create derived files.
212  // Glob for the pattern mentioned above.
213  final String glob = String.format("{%1$s.jpg,%1$s-*.jpg}", baseFileName);
214  try (DirectoryStream<Path> stream = Files.newDirectoryStream(moduleOutputFolder, glob)) {
215 
216  final Path caseDirectory = Paths.get(Case.getCurrentCaseThrows().getCaseDirectory());
217  for (Path candidate : stream) {
218  if (context.fileIngestIsCancelled()) {
219  return;
220  }
221 
222  final BasicFileAttributes attrs = Files.readAttributes(candidate, BasicFileAttributes.class);
223  final Path localCasePath = caseDirectory.relativize(candidate);
224 
225  final DerivedFile jpegFile = Case.getCurrentCaseThrows().getSleuthkitCase()
226  .addDerivedFile(candidate.getFileName().toString(),
227  localCasePath.toString(), attrs.size(), 0L,
228  attrs.creationTime().to(TimeUnit.SECONDS),
229  attrs.lastAccessTime().to(TimeUnit.SECONDS),
230  attrs.lastModifiedTime().to(TimeUnit.SECONDS),
231  attrs.isRegularFile(), heicFile, "",
232  "", "", "", TskData.EncodingType.NONE);
233 
234  context.addFilesToJob(Arrays.asList(jpegFile));
236  }
237 
238  } catch (DirectoryIteratorException ex) {
239  throw ex.getCause();
240  }
241  }
242 
243  @Override
244  public Set<String> mimeTypes() {
245  return new HashSet<String>() {
246  {
247  add("image/heif");
248  add("image/heic");
249  }
250  };
251  }
252 }
static int execute(ProcessBuilder processBuilder)
Definition: ExecUtil.java:145
void fireModuleContentEvent(ModuleContentEvent moduleContentEvent)
void convertToJPEG(IngestJobContext context, Path localDiskCopy, AbstractFile heicFile)
static String escapeFileName(String fileName)
Definition: FileUtil.java:169
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
void process(IngestJobContext context, AbstractFile file)
static synchronized IngestServices getInstance()

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.