/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.tests.unit.chunk;
import static org.junit.Assert.assertArrayEquals;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.NoSuchPaddingException;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Test;
import org.syncany.chunk.Chunk;
import org.syncany.chunk.Chunker;
import org.syncany.chunk.CipherTransformer;
import org.syncany.chunk.Deduper;
import org.syncany.chunk.DeduperListener;
import org.syncany.chunk.GzipTransformer;
import org.syncany.chunk.MultiChunk;
import org.syncany.chunk.MultiChunker;
import org.syncany.chunk.NoTransformer;
import org.syncany.chunk.Transformer;
import org.syncany.chunk.TttdChunker;
import org.syncany.chunk.ZipMultiChunker;
import org.syncany.config.Logging;
import org.syncany.crypto.CipherException;
import org.syncany.crypto.CipherSpec;
import org.syncany.crypto.CipherSpecs;
import org.syncany.crypto.CipherUtil;
import org.syncany.crypto.SaltedSecretKey;
import org.syncany.database.ChunkEntry.ChunkChecksum;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
import org.syncany.tests.unit.util.TestFileUtil;
import org.syncany.util.FileUtil;
import org.syncany.util.StringUtil;
public class FrameworkCombinationTest {
private static final Logger logger = Logger.getLogger(FrameworkCombinationTest.class.getSimpleName());
private File tempDir;
private List<FrameworkCombination> combinations;
private SaltedSecretKey masterKey;
static {
Logging.init();
}
@Before
public void initMasterKey() throws CipherException {
masterKey = CipherUtil.createMasterKey("some password");
}
@Test
public void testBlackBoxCombinationsWith50KBInputFile() throws Exception {
// Setup
setup();
// Test
List<File> inputFiles = TestFileUtil.createRandomFilesInDirectory(tempDir, 10 * 1024, 5);
for (FrameworkCombination combination : combinations) {
logger.info("");
logger.info("Testing framework combination " + combination.name + " ...");
logger.info("---------------------------------------------------------------");
testBlackBoxCombination(inputFiles, combination);
}
// Tear down (if success)
teardown();
}
public void setup() throws Exception {
tempDir = TestFileUtil.createTempDirectoryInSystemTemp();
combinations = new ArrayList<FrameworkCombination>();
fillCombinations();
}
private void fillCombinations() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
// MultiChunks
List<MultiChunker> multiChunkers = new LinkedList<MultiChunker>();
int[] multiChunkSizes = new int[] { 250000, 500000 };
for (int i = 0; i < multiChunkSizes.length; i++) {
//multiChunkers.add(new CustomMultiChunker(multiChunkSizes[i]));
multiChunkers.add(new ZipMultiChunker(multiChunkSizes[i]));
}
// Chunks
List<Chunker> chunkers = new LinkedList<Chunker>();
int[] chunkSizes = new int[] { 8000, 16000 };
String[] digestAlgs = new String[] { /*"MD5" ,*/"SHA1" };
String[] fingerprinters = new String[] { "Adler32" /*, "Plain", "Rabin"*/};
for (int i = 0; i < chunkSizes.length; i++) {
for (int j = 0; j < digestAlgs.length; j++) {
//chunkers.add(new FixedOffsetChunker(chunkSizes[i], digestAlgs[j]));
for (int k = 0; k < fingerprinters.length; k++) {
chunkers.add(new TttdChunker(chunkSizes[i], TttdChunker.DEFAULT_WINDOW_SIZE, digestAlgs[j], fingerprinters[k]));
}
}
}
// Compression/Encryption
List<CipherSpec> cipherSpecs = new ArrayList<CipherSpec>();
cipherSpecs.add(CipherSpecs.getCipherSpec(1));
cipherSpecs.add(CipherSpecs.getCipherSpec(2));
List<Transformer> transformerChains = new LinkedList<Transformer>();
transformerChains.add(new NoTransformer());
transformerChains.add(new GzipTransformer());
transformerChains.add(new CipherTransformer(cipherSpecs, masterKey));
transformerChains.add(new GzipTransformer(new CipherTransformer(cipherSpecs, masterKey)));
for (MultiChunker multiChunker : multiChunkers) {
for (Transformer transformer : transformerChains) {
for (Chunker chunker : chunkers) {
String configName = multiChunker + "/" + chunker + "/" + transformer;
combinations.add(new FrameworkCombination(configName, chunker, multiChunker, transformer));
}
}
}
}
public void teardown() throws Exception {
TestFileUtil.deleteDirectory(tempDir);
}
private void testBlackBoxCombination(List<File> inputFiles, FrameworkCombination combination) throws Exception {
// Deduplicate
ChunkIndex chunkIndex = deduplicateAndCreateChunkIndex(inputFiles, combination);
// Assemble
Map<ChunkChecksum, File> extractedChunkIDToChunkFile = extractChunksFromMultiChunks(chunkIndex.outputMultiChunkFiles, combination);
Map<File, File> inputFilesToReassembledOutputFiles = reassembleFiles(chunkIndex.inputFileToChunkIDs, extractedChunkIDToChunkFile);
// Compare checksums of files
for (Map.Entry<File, File> inputFilesToReassembledOutputFilesEntry : inputFilesToReassembledOutputFiles.entrySet()) {
File inputFile = inputFilesToReassembledOutputFilesEntry.getKey();
File outputFile = inputFilesToReassembledOutputFilesEntry.getValue();
byte[] inputFileChecksum = TestFileUtil.createChecksum(inputFile);
byte[] outputFileChecksum = TestFileUtil.createChecksum(outputFile);
assertArrayEquals("Input file and output file checksums do not match" +
"for files " + inputFile + " and " + outputFile, inputFileChecksum, outputFileChecksum);
}
}
private ChunkIndex deduplicateAndCreateChunkIndex(final List<File> inputFiles, FrameworkCombination combination) throws IOException {
logger.log(Level.INFO, "- Deduplicate and create chunk index ...");
final ChunkIndex chunkIndex = new ChunkIndex();
Deduper deduper = new Deduper(combination.chunker, combination.multiChunker, combination.transformer, Long.MAX_VALUE, Long.MAX_VALUE);
deduper.deduplicate(inputFiles, new DeduperListener() {
@Override
public void onMultiChunkWrite(MultiChunk multiChunk, Chunk chunk) {
logger.log(Level.INFO, " - Adding chunk " + StringUtil.toHex(chunk.getChecksum()) + " to multichunk " + multiChunk.getId()
+ " ...");
chunkIndex.chunkIDToMultiChunkID.put(new ChunkChecksum(chunk.getChecksum()), multiChunk.getId());
}
@Override
public void onFileAddChunk(File file, Chunk chunk) {
logger.log(Level.INFO, " - Adding chunk " + StringUtil.toHex(chunk.getChecksum()) + " to inputFileToChunkIDs-map for file " + file
+ " ...");
List<ChunkChecksum> chunkIDsForFile = chunkIndex.inputFileToChunkIDs.get(file);
if (chunkIDsForFile == null) {
chunkIDsForFile = new ArrayList<ChunkChecksum>();
}
chunkIDsForFile.add(new ChunkChecksum(chunk.getChecksum()));
chunkIndex.inputFileToChunkIDs.put(file, chunkIDsForFile);
}
@Override
public boolean onChunk(Chunk chunk) {
if (chunkIndex.chunkIDToMultiChunkID.containsKey(new ChunkChecksum(chunk.getChecksum()))) {
logger.log(Level.INFO, " + Known chunk " + StringUtil.toHex(chunk.getChecksum()));
return false;
}
else {
logger.log(Level.INFO, " + New chunk " + StringUtil.toHex(chunk.getChecksum()));
return true;
}
}
@Override
public File getMultiChunkFile(MultiChunkId multiChunkId) {
File outputMultiChunk = new File(tempDir + "/multichunk-" + multiChunkId);
chunkIndex.outputMultiChunkFiles.add(outputMultiChunk);
return outputMultiChunk;
}
@Override
public MultiChunkId createNewMultiChunkId(Chunk firstChunk) {
// Note: In the real implementation, this should be random
return new MultiChunkId(firstChunk.getChecksum());
}
@Override
public boolean onFileFilter(File file) {
return true;
}
@Override
public boolean onFileStart(File file) {
return file.isFile() && !FileUtil.isSymlink(file);
}
@Override
public void onFileEnd(File file, byte[] checksum) {
// Empty
}
@Override
public void onMultiChunkOpen(MultiChunk multiChunk) {
// Empty
}
@Override
public void onMultiChunkClose(MultiChunk multiChunk) {
// Empty
}
@Override
public void onStart(int fileCount) {
// Empty
}
@Override
public void onFinish() {
// Empty
}
});
return chunkIndex;
}
private Map<ChunkChecksum, File> extractChunksFromMultiChunks(List<File> outputMultiChunkFiles, FrameworkCombination combination)
throws IOException {
Map<ChunkChecksum, File> extractedChunks = new HashMap<ChunkChecksum, File>();
for (File outputMultiChunkFile : outputMultiChunkFiles) {
logger.log(Level.INFO, "- Extracting multichunk " + outputMultiChunkFile + " ...");
MultiChunk outputMultiChunk = combination.multiChunker.createMultiChunk(
combination.transformer.createInputStream(new FileInputStream(outputMultiChunkFile)));
Chunk outputChunkInMultiChunk = null;
while (null != (outputChunkInMultiChunk = outputMultiChunk.read())) {
File extractedChunkFile = new File(tempDir + "/chunk-" + StringUtil.toHex((outputChunkInMultiChunk.getChecksum()))
+ "-from-multichunk-" + outputMultiChunk.getId());
logger.log(Level.INFO, " + Writing chunk " + StringUtil.toHex((outputChunkInMultiChunk.getChecksum())) + " to " + extractedChunkFile
+ " ...");
TestFileUtil.writeToFile(outputChunkInMultiChunk.getContent(), extractedChunkFile);
extractedChunks.put(new ChunkChecksum(outputChunkInMultiChunk.getChecksum()), extractedChunkFile);
}
}
return extractedChunks;
}
private Map<File, File> reassembleFiles(Map<File, List<ChunkChecksum>> inputFileToChunkIDs, Map<ChunkChecksum, File> extractedChunkIDToChunkFile)
throws IOException {
Map<File, File> inputFileToOutputFile = new HashMap<File, File>();
for (Map.Entry<File, List<ChunkChecksum>> inputFileToChunkIDsEntry : inputFileToChunkIDs.entrySet()) {
File inputFile = inputFileToChunkIDsEntry.getKey();
List<ChunkChecksum> chunkIDs = inputFileToChunkIDsEntry.getValue();
File outputFile = new File(tempDir + "/reassembledfile-" + inputFile.getName());
FileOutputStream outputFileOutputStream = new FileOutputStream(outputFile);
logger.log(Level.INFO, "- Reassemble file " + inputFile + " to " + outputFile + " ...");
for (ChunkChecksum chunkID : chunkIDs) {
File extractedChunkFile = extractedChunkIDToChunkFile.get(chunkID);
logger.log(Level.INFO, " + Appending " + chunkID + " (file: " + extractedChunkFile + ") to " + outputFile + " ...");
IOUtils.copy(new FileInputStream(extractedChunkFile), outputFileOutputStream);
}
inputFileToOutputFile.put(inputFile, outputFile);
}
return inputFileToOutputFile;
}
private static class FrameworkCombination {
private String name;
private Chunker chunker;
private MultiChunker multiChunker;
private Transformer transformer;
public FrameworkCombination(String name, Chunker chunker, MultiChunker multiChunker, Transformer transformerChain) {
this.name = name;
this.chunker = chunker;
this.multiChunker = multiChunker;
transformer = transformerChain;
}
}
private static class ChunkIndex {
private Map<File, List<ChunkChecksum>> inputFileToChunkIDs = new HashMap<File, List<ChunkChecksum>>();
private Map<ChunkChecksum, MultiChunkId> chunkIDToMultiChunkID = new HashMap<ChunkChecksum, MultiChunkId>();
private List<File> outputMultiChunkFiles = new ArrayList<File>();
}
}