blob: 226eeda8b239d02e5dfefa45ace09e8a3c403853 [file] [log] [blame]
/**
********************************************************************************
* Copyright (c) 2021-2022 Robert Bosch GmbH and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Robert Bosch GmbH - initial API and implementation
********************************************************************************
*/
package org.eclipse.app4mc.amalthea.model.editor.handler;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.inject.Named;
import org.eclipse.app4mc.amalthea.model.editor.container.AmaltheaModelContainer;
import org.eclipse.app4mc.amalthea.model.editor.util.AmaltheaEditorUtil;
import org.eclipse.app4mc.amalthea.model.emf.AmaltheaResource;
import org.eclipse.app4mc.amalthea.model.io.AmaltheaFileHelper;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.e4.core.di.annotations.Execute;
import org.eclipse.e4.ui.services.IServiceConstants;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IDecoratorManager;
import org.eclipse.ui.PlatformUI;
public class UncompressHandler {
private static final int BUFFER_SIZE = 2048;
private static final long MAX_FILE_SIZE = 2_000_000_000L; // 2 GB
private static final String COMPRESS_ERROR = "Compress error";
@Execute
public void execute(@Named(IServiceConstants.ACTIVE_SELECTION) IStructuredSelection selection, Shell shell, IDecoratorManager decoratorManager) {
Object firstElement = selection.getFirstElement();
if (firstElement instanceof IFile) {
IFile iFile = (IFile) firstElement;
if (AmaltheaFileHelper.isModelFileExtension(iFile.getFileExtension())) {
AmaltheaModelContainer modelContainer = AmaltheaEditorUtil.getModelContainer(iFile);
if (modelContainer != null && modelContainer.getInitialModelFiles().contains(iFile)) {
// Resource is available (model is in memory)
markResourceUnzipped(iFile, modelContainer);
// Give feedback to user (because file will not be changed immediately)
showInfoDialog();
} else {
// Unzip model file
unzipModelFile(iFile, shell, decoratorManager);
}
}
}
}
private void showInfoDialog() {
Display.getDefault().asyncExec( () -> MessageDialog.openInformation(
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
"Uncompress",
"Resource successfully marked as \"expanded\".\n\nSave the model to persist changes.") );
}
private void markResourceUnzipped(IFile iFile, AmaltheaModelContainer modelContainer) {
ResourceSet resourceSet = modelContainer.getResourceSet();
if (resourceSet != null) {
for (Resource resource : resourceSet.getResources()) {
if (resource instanceof AmaltheaResource && resource.getURI().lastSegment().equals(iFile.getName())) {
// Modify zip flag of loaded resource
((AmaltheaResource) resource).setUseZip(false);
// Mark resource as modified
resource.setModified(true);
// Mark editors dirty
modelContainer.updateDirtyStateOfEditors();
break;
}
}
}
}
/**
* Extracts first file of Zip archive (generic implementation based on java.util.zip package)
*
* Following vulnerabilities are handled:
* - Zip Slip (https://snyk.io/research/zip-slip-vulnerability)
* - Zip Bomb (https://wiki.sei.cmu.edu/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream)
*
* @param iFile
* @param shell
* @param decoratorManager
*/
private void unzipModelFile(IFile iFile, Shell shell, IDecoratorManager decoratorManager) {
if (!AmaltheaEditorUtil.isZipFile(iFile)) {
MessageDialog.openError(shell, COMPRESS_ERROR, "Invalid file!");
return; // stop further actions
}
File modelFile = AmaltheaEditorUtil.getLocalFile(iFile);
File modelFileTmp = new File(modelFile.getParentFile(), modelFile.getName() + ".tmp");
// Unzip model file (write first entry to temporary file)
try ( ZipInputStream inputStream = new ZipInputStream(new FileInputStream(modelFile));
FileOutputStream outputStream = new FileOutputStream(modelFileTmp);)
{
ZipEntry zipEntry = inputStream.getNextEntry();
if (zipEntry != null && !zipEntry.isDirectory()) {
long total = 0;
byte[] bytes = new byte[BUFFER_SIZE];
int length;
while((length = inputStream.read(bytes)) > 0) {
// check size of extracted data (to avoid Zip bombs)
if (total + length > MAX_FILE_SIZE) {
MessageDialog.openError(shell, COMPRESS_ERROR, "File being unzipped is too big.");
return; // stop further actions
}
outputStream.write(bytes, 0, length);
total += length;
}
}
} catch (IOException e) {
MessageDialog.openError(shell, COMPRESS_ERROR, "Failed to uncompress model file! " + e.getMessage());
return; // stop further actions
}
// Delete old model file and rename temporary file
try {
Files.delete(modelFile.toPath());
Files.move(modelFileTmp.toPath(), modelFile.toPath());
} catch (IOException e) {
MessageDialog.openError(shell, COMPRESS_ERROR, "Failed to replace model file! " + e.getMessage());
}
// Refresh file and decorators
try {
iFile.refreshLocal(IResource.DEPTH_INFINITE, null);
decoratorManager.update("org.eclipse.app4mc.amalthea.file.compressed.decorator");
} catch (CoreException e) {
Platform.getLog(getClass()).error(e.getLocalizedMessage(), e);
}
}
}