/** * Copyright (c) 2015 CommonsWare, LLC * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.commonsware.android.backup; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; // inspired by https://www.securecoding.cert.org/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream // modified from https://github.com/commonsguy/cwac-security class ZipUtils { private static final int BUFFER_SIZE=16384; private static final int DEFAULT_MAX_ENTRIES=1024; private static final int DEFAULT_MAX_SIZE=1024*1024*64; public static void unzip(File zipFile, File destDir, String subtreeInZip) throws UnzipException, IOException { if (destDir.exists()) { deleteContents(destDir); } else { destDir.mkdirs(); } try { final FileInputStream fis=new FileInputStream(zipFile); final ZipInputStream zis=new ZipInputStream(new BufferedInputStream(fis)); ZipEntry entry; int entries=0; long total=0; try { while ((entry=zis.getNextEntry()) != null) { if (subtreeInZip==null || entry.getName().startsWith(subtreeInZip)) { int bytesRead; final byte data[]=new byte[BUFFER_SIZE]; final String zipCanonicalPath= validateZipEntry(entry.getName().substring(subtreeInZip.length()), destDir); if (entry.isDirectory()) { new File(zipCanonicalPath).mkdir(); } else { final FileOutputStream fos= new FileOutputStream(zipCanonicalPath); final BufferedOutputStream dest= new BufferedOutputStream(fos, BUFFER_SIZE); while (total+BUFFER_SIZE<=DEFAULT_MAX_SIZE && (bytesRead=zis.read(data, 0, BUFFER_SIZE))!=-1) { dest.write(data, 0, bytesRead); total+=bytesRead; } dest.flush(); fos.getFD().sync(); dest.close(); if (total+BUFFER_SIZE>DEFAULT_MAX_SIZE) { throw new IllegalStateException( "Too much output from ZIP"); } } zis.closeEntry(); entries++; if (entries>DEFAULT_MAX_ENTRIES) { throw new IllegalStateException( "Too many entries in ZIP"); } } } } finally { zis.close(); } } catch (Throwable t) { if (destDir.exists()) { delete(destDir); } throw new UnzipException("Problem in unzip operation, rolling back", t); } } // inspired by http://pastebin.com/PqJyzQUx public static boolean delete(File f) { if (f.isDirectory()) { for (File child : f.listFiles()) { if (!delete(child)) { return(false); } } } return(f.delete()); } public static boolean deleteContents(File f) { if (f.isDirectory()) { for (File child : f.listFiles()) { if (!delete(child)) { return(false); } } } return(true); } private static String validateZipEntry(String zipEntryRelativePath, File destDir) throws IOException { File zipEntryTarget=new File(destDir, zipEntryRelativePath); String zipCanonicalPath=zipEntryTarget.getCanonicalPath(); if (zipCanonicalPath.startsWith(destDir.getCanonicalPath())) { return(zipCanonicalPath); } throw new IllegalStateException("ZIP entry tried to write outside destination directory"); } public static class UnzipException extends Exception { public UnzipException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } }