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