Commit 7d1a15a6 authored by Matyáš Latner's avatar Matyáš Latner

#14 Check of uzipped assets integrity

 * mechanism for calculating MD5 checksum for all directory (same mechanism used in ZipLoader and Gradle)
parent 96e8cad1
final ASSETS_CHECKSUM_FIELD_TYPE = "String";
final ASSETS_CHECKSUM_FIELD_NAME = "ASSETS_CHECKSUMS";
final BUILD_CONFIG_DIR = "${buildDir}/generated/source/buildConfig/"
final BUILD_CONFIG_ENCODING = 'UTF-8'
android {
buildToolsVersion "21.1.2"
compileSdkVersion 21
......@@ -5,13 +10,16 @@ android {
buildTypes {
debug {
applicationIdSuffix rootProject.applicationIdDebugSuffix
buildConfigField ASSETS_CHECKSUM_FIELD_TYPE, ASSETS_CHECKSUM_FIELD_NAME, rootProject.ext.assetsChecksumPattern
}
release {
debuggable false
buildConfigField ASSETS_CHECKSUM_FIELD_TYPE, ASSETS_CHECKSUM_FIELD_NAME, rootProject.ext.assetsChecksumPattern
}
devel.initWith(buildTypes.debug)
devel {
applicationIdSuffix rootProject.applicationIdDevelSuffix
buildConfigField ASSETS_CHECKSUM_FIELD_TYPE, ASSETS_CHECKSUM_FIELD_NAME, rootProject.ext.assetsChecksumPattern
}
}
......@@ -77,6 +85,54 @@ android {
}
}
// hack to add assets checksum to BuildConfig -> build config fields are generated at script startup
// -> calculated values from script runtime have to be added to file manually
tasks.whenTaskAdded { compileTask ->
// select all "compile{BUILD_TYPE}Java" android tasks
def matcher = (compileTask.name =~ 'compile(.+)Java')
if (matcher.matches()) {
String buildName = matcher[0][1]
// without test tasks
if (!buildName.contains("Test")) {
// for all selected tasks create dependency task "checksum{BUILD_TYPE}Assets"
task("checksum${buildName}Assets") {
// calculated checksum is dependency
dependsOn(':util:checksum:runJarChecksum')
// wait for generating android sources
dependsOn("generate${buildName}Sources")
// modify BuildConfig.java -> write checksum in
doLast{
ConfigurableFileTree cft = getProject().fileTree(BUILD_CONFIG_DIR + buildName.toLowerCase())
cft.include("**/BuildConfig.java")
Iterator<File> iter = cft.iterator()
while (iter.hasNext()){
File buildConfigFile = iter.next()
String buildConfigContent = buildConfigFile.getText(BUILD_CONFIG_ENCODING)
/*
String checksums = "";
rootProject.ext.assetsChecksum.each { key, value ->
if (checksums.length() > 0) {
checksums = checksums + ", ";
}
checksums = checksums + "\"${key}\", \"${value}\""
}
*/
buildConfigContent = buildConfigContent.replace(rootProject.ext.assetsChecksumPattern, '\"' + rootProject.ext.assetsChecksum.toString() + '\"')
buildConfigFile.write(buildConfigContent, BUILD_CONFIG_ENCODING)
}
}
}
// set as dependency for "compile{BUILD_TYPE}Java" task
compileTask.dependsOn("checksum${buildName}Assets")
}
}
}
// needed to add JNI shared libraries to APK when compiling on CLI
tasks.withType(com.android.build.gradle.tasks.PackageApplication) { pkgTask ->
pkgTask.jniFolders = new HashSet<File>()
......@@ -125,6 +181,7 @@ task run(type: Exec) {
def adb = path + "/platform-tools/adb"
commandLine "$adb", 'shell', 'am', 'start', '-n', 'cz.nic.tablexia.android/cz.nic.tablexia.android.AndroidLauncher'
}
// sets up the Android Eclipse project, using the old Ant based build.
eclipse {
// need to specify Java source sets explicitely, SpringSource Gradle Eclipse plugin
......
......@@ -7,6 +7,7 @@ import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import cz.nic.tablexia.Tablexia;
import cz.nic.tablexia.debug.BuildConfig;
import cz.nic.tablexia.util.Utility;
public class AndroidLauncher extends AndroidApplication {
......@@ -20,6 +21,7 @@ public class AndroidLauncher extends AndroidApplication {
getResources().getConfiguration().locale,
BuildConfig.VERSION_NAME,
SQL_CONNECTION_TYPE,
Utility.createChecksumMapFromString(BuildConfig.ASSETS_CHECKSUMS, ":"),
savedInstanceState == null), config);
}
}
......@@ -10,6 +10,11 @@ buildscript {
}
}
ext {
assetsChecksumPattern = "=======ASSETS_CHECKSUM======="
assetsChecksum = [:]
}
allprojects {
apply plugin: "eclipse"
apply plugin: "idea"
......@@ -35,6 +40,7 @@ allprojects {
aiVersion = '1.4.0'
sqlDroidVersion = '1.0.3'
sqlLiteJdbcVersion = '3.8.10.1'
guavaVersion = '18.0'
}
repositories {
......@@ -62,16 +68,8 @@ def getVersionCodeFromGit() {
return stdout.toString().trim().toInteger()
}
task deleteZips(type: Delete) {
new File(project(":android").projectDir.absolutePath + "/assets/").eachFile() { file ->
if (file.getName().endsWith(".zip")) {
delete file.absolutePath
}
}
}
task zipAssets(type:Zip, dependsOn: deleteZips) {
task zipAssets(type:Zip) {
outputs.upToDateWhen { false }
new File(project(":core").projectDir.absolutePath + "/assets").eachDir() { dir ->
task(dir.getName(), type: Zip) {
......@@ -162,10 +160,12 @@ project(":core") {
tasks.processResources.dependsOn zipAssets
dependencies {
compile project(":util:checksum")
compile "com.badlogicgames.gdx:gdx:$gdxVersion"
compile "net.dermetfan.libgdx-utils:libgdx-utils:$gdxUtilsVersion"
compile "net.engio:mbassador:$mbassadorVersion"
compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
compile "com.google.guava:guava:$guavaVersion"
testCompile "junit:junit:4.11"
testCompile "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion"
......
......@@ -6,6 +6,7 @@ import com.badlogic.gdx.Gdx;
import net.engio.mbassy.listener.Handler;
import java.util.Locale;
import java.util.Map;
import cz.nic.tablexia.bus.ApplicationBus;
import cz.nic.tablexia.bus.ApplicationBus.ApplicationEvent;
......@@ -27,6 +28,7 @@ public class Tablexia extends TablexiaApplication {
private boolean loadingComplete = false;
private MainMenuContainer mainMenuContainer;
private ZipAssetLoader zipAssetLoader;
private Map<String, String> buildChecksums;
private boolean resetState;
public static class SQLConnectionType {
......@@ -48,15 +50,19 @@ public class Tablexia extends TablexiaApplication {
}
}
public Tablexia(boolean debug, Locale systemLocale, String versionName, SQLConnectionType sqlConnectionType, boolean resetState) {
public Tablexia(boolean debug, Locale systemLocale, String versionName, SQLConnectionType sqlConnectionType, Map<String, String> buildChecksums, boolean resetState) {
this(sqlConnectionType, buildChecksums, resetState);
TablexiaSettings.init(debug, systemLocale, versionName);
this.resetState = resetState;
this.sqlConnectionType = sqlConnectionType;
}
public Tablexia(String buildTypeKey, Locale systemLocale, String versionName, SQLConnectionType sqlConnectionType, boolean resetState) {
public Tablexia(String buildTypeKey, Locale systemLocale, String versionName, SQLConnectionType sqlConnectionType, Map<String, String> buildChecksums, boolean resetState) {
this(sqlConnectionType, buildChecksums, resetState);
TablexiaSettings.init(buildTypeKey, systemLocale, versionName);
}
private Tablexia(SQLConnectionType sqlConnectionType, Map<String, String> buildChecksums, boolean resetState) {
this.resetState = resetState;
this.buildChecksums = buildChecksums;
this.sqlConnectionType = sqlConnectionType;
}
......@@ -91,7 +97,7 @@ public class Tablexia extends TablexiaApplication {
ApplicationTextManager.getInstance().load(locale);
// async zip extraction
zipAssetLoader = new ZipAssetLoader();
zipAssetLoader.load(locale);
zipAssetLoader.load(locale, buildChecksums);
// async external assets loading
ApplicationTextureManager.getInstance().load();
ApplicationSoundManager.getInstance().load();
......
......@@ -2,7 +2,6 @@ package cz.nic.tablexia.loader.zip;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.I18NBundle;
import com.badlogic.gdx.utils.async.AsyncTask;
import java.io.BufferedInputStream;
......@@ -10,13 +9,17 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import cz.nic.tablexia.checksum.Checksum;
import cz.nic.tablexia.loader.IApplicationLoader;
import cz.nic.tablexia.loader.TablexiaAssetManager;
import cz.nic.tablexia.loader.TablexiaDataManager;
import cz.nic.tablexia.util.Log;
/**
* Zip assets content loader
......@@ -33,20 +36,45 @@ public class ZipAssetLoader extends TablexiaDataManager<Void> implements IApplic
private static class ZipAssetLoaderTask implements AsyncTask<Void> {
private Locale locale;
private Map<String, String> buildChecksum;
public ZipAssetLoaderTask(Locale locale) {
public ZipAssetLoaderTask(Locale locale, Map<String, String> buildChecksum) {
this.locale = locale;
}
this.buildChecksum = buildChecksum;
}
private boolean checkAssets(String buildChecksum, String extractDestinationDirectory) {
File file = new File(extractDestinationDirectory);
if (file.exists()) {
try {
String runtimeChecksum = Checksum.getMd5OfDir(file.getAbsolutePath());
Log.debug(getClass(), "Comparing assets checksums: [BUILD: " + buildChecksum + "] - [RUNTIME: " + runtimeChecksum + "]");
return runtimeChecksum.equals(buildChecksum);
} catch (NoSuchAlgorithmException e) {
Log.err(getClass(), "Cannot get checksum for assets!", e);
} catch (IOException e) {
Log.err(getClass(), "Cannot get checksum for assets!", e);
}
}
return false;
}
@Override
public Void call() throws Exception {
String language = locale.getLanguage();
String commonZipFile = COMMON_ZIP_FILE_NAME + ZIP_FILE_EXTENSION;
String localisedZipFile = locale.getLanguage() + ZIP_FILE_EXTENSION;
String localisedZipFile = language + ZIP_FILE_EXTENSION;
String extractDestinationDirectory = Gdx.files.getExternalStoragePath() + TablexiaAssetManager.StorageType.EXTERNAL.getStoragePath();
unzip(ZIP_FILES_STORAGE_TYPE.getResolver().resolve(ZIP_FILES_STORAGE_TYPE.getStoragePath() + commonZipFile), extractDestinationDirectory);
unzip(ZIP_FILES_STORAGE_TYPE.getResolver().resolve(ZIP_FILES_STORAGE_TYPE.getStoragePath() + localisedZipFile), extractDestinationDirectory);
if (buildChecksum == null || !checkAssets(buildChecksum.get(language), extractDestinationDirectory)) {
Log.info(getClass(), "Assets check FAILED! --> Extracting new assets");
unzip(ZIP_FILES_STORAGE_TYPE.getResolver().resolve(ZIP_FILES_STORAGE_TYPE.getStoragePath() + commonZipFile), extractDestinationDirectory);
unzip(ZIP_FILES_STORAGE_TYPE.getResolver().resolve(ZIP_FILES_STORAGE_TYPE.getStoragePath() + localisedZipFile), extractDestinationDirectory);
} else {
Log.info(getClass(), "Assets check OK!");
}
return null;
}
......@@ -87,8 +115,8 @@ public class ZipAssetLoader extends TablexiaDataManager<Void> implements IApplic
}
}
public void load(Locale locale) {
setAsyncTask(new ZipAssetLoaderTask(locale));
public void load(Locale locale, Map<String, String> buildChecksum) {
setAsyncTask(new ZipAssetLoaderTask(locale, buildChecksum));
}
}
\ No newline at end of file
......@@ -2,6 +2,9 @@ package cz.nic.tablexia.util;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.ReflectionException;
import com.google.common.base.Splitter;
import java.util.Map;
import cz.nic.tablexia.screen.AbstractTablexiaScreen;
......@@ -16,4 +19,12 @@ public class Utility {
return null;
}
public static Map<String, String> createChecksumMapFromString(String checksumsString, String keyValueSeparator) {
if (checksumsString == null) {
return null;
}
String checksums = checksumsString.substring(1, checksumsString.length() - 1);
return Splitter.on(',').trimResults().withKeyValueSeparator(keyValueSeparator).split(checksums);
}
}
......@@ -21,10 +21,13 @@ task debugJar(type: Jar) {
from {configurations.compile.collect {zipTree(it)}}
from files(project.assetsDir);
manifest {
attributes 'Main-Class': project.mainClassName
attributes 'Build-Type': 'debug'
attributes 'Version-Name': tablexiaVersionName
doFirst {
manifest {
attributes 'Main-Class': project.mainClassName
attributes 'Build-Type': 'debug'
attributes 'Version-Name': tablexiaVersionName
attributes 'Assets-Cheksums': rootProject.ext.assetsChecksum.toString()
}
}
}
......@@ -35,10 +38,13 @@ task releaseJar(type: Jar) {
from {configurations.compile.collect {zipTree(it)}}
from files(project.assetsDir);
manifest {
attributes 'Main-Class': project.mainClassName
attributes 'Build-Type': 'release'
attributes 'Version-Name': tablexiaVersionName
doFirst {
manifest {
attributes 'Main-Class': project.mainClassName
attributes 'Build-Type': 'release'
attributes 'Version-Name': tablexiaVersionName
attributes 'Assets-Cheksums': rootProject.ext.assetsChecksum
}
}
}
......@@ -48,7 +54,9 @@ artifacts {
jar.enabled = false
debugJar.dependsOn classes
debugJar.dependsOn(':util:checksum:runJarChecksum')
releaseJar.dependsOn classes
releaseJar.dependsOn(':util:checksum:runJarChecksum')
eclipse {
project {
......
......@@ -9,11 +9,13 @@ import java.util.Locale;
import cz.nic.tablexia.Tablexia;
import cz.nic.tablexia.TablexiaSettings;
import cz.nic.tablexia.util.Utility;
public class DesktopLauncher {
private static final String BUILD_VARIANT_MANIFEST_ATTRIBUTE = "Build-Type";
private static final String VERSION_NAME_MANIFEST_ATTRIBUTE = "Version-Name";
private static final String ASSETS_CHECKSUMS_MANIFEST_ATTRIBUTE = "Assets-Cheksums";
private static final String DESKTOP_ICON_PATH = "icon/";
private static final String DESKTOP_ICON_16 = DESKTOP_ICON_PATH + "desktop_icon_16.png";
......@@ -32,6 +34,7 @@ public class DesktopLauncher {
String buildType = loadAttributeFromManifest(BUILD_VARIANT_MANIFEST_ATTRIBUTE);
String versionName = loadAttributeFromManifest(VERSION_NAME_MANIFEST_ATTRIBUTE);
String checksums = loadAttributeFromManifest(ASSETS_CHECKSUMS_MANIFEST_ATTRIBUTE);
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.resizable = buildType == null || buildType.equals(TablexiaSettings.BuildType.DEVEL.getKey());
......@@ -56,6 +59,7 @@ public class DesktopLauncher {
Locale.getDefault(),
versionName,
SQL_CONNECTION_TYPE,
Utility.createChecksumMapFromString(checksums, "="),
true), config);
}
......
include 'desktop', 'android', 'ios', 'core'
include 'desktop', 'android', 'ios', 'core', 'util:checksum'
final CHECKSUM_DIR = "/checksum/"
final LIBS_DIR = "${buildDir}/libs/"
final JAR_NAME = "checksum.jar"
final MAIN_CLASS = "cz.nic.tablexia.checksum.Checksum"
apply plugin: "java"
sourceSets.main.java.srcDirs = [ "src/" ]
jar() {
archiveName = JAR_NAME
manifest {
attributes 'Main-Class': MAIN_CLASS
}
}
task runJarChecksum(dependsOn: jar) {
doLast {
def sourceDir = new File(project(":core").projectDir.absolutePath + "/assets")
sourceDir.eachDir() { dir ->
if (!dir.name.equals('common')) {
String dirName = dir.getName()
String destDir = "" + buildDir + CHECKSUM_DIR + dirName
task("checksum_" + dirName, type: Copy) {
println dir.absolutePath
from sourceDir.absolutePath + "/common"
from dir.absolutePath
into destDir
}.execute()
task ("runJarChecksum_" + dir.getName(), type: JavaExec) {
classpath = files(LIBS_DIR + JAR_NAME)
classpath += sourceSets.main.runtimeClasspath
main = MAIN_CLASS
args = [destDir]
def stdout = new ByteArrayOutputStream()
standardOutput = stdout
doLast {
println "TEST: " + rootProject.ext.assetsChecksum[dirName]
rootProject.ext.assetsChecksum[dirName] = stdout.toString().trim()
println "TEST: " + rootProject.ext.assetsChecksum[dirName]
}
}.execute()
}
}
}
}
package cz.nic.tablexia.checksum;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Checksum {
public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
System.out.println(getMd5OfDir(args[0]));
}
public static String getMd5OfDir(String dir) throws NoSuchAlgorithmException, IOException {
String md5 = "";
File folder = new File(dir);
File[] files = folder.listFiles();
for (int i = 0; i < files.length; i++) {
if ("tablexia.db".equals(files[i].getName())) {
continue;
} else if (files[i].isDirectory()) {
md5 = md5 + getMd5OfDir(files[i].getAbsolutePath());
} else {
md5 = md5 + getMd5OfFile(files[i].toString());
}
}
return getMD5OfString(md5);
}
public static String getMd5OfFile(String filePath) throws IOException, NoSuchAlgorithmException {
String returnVal = "";
InputStream input = new FileInputStream(filePath);
byte[] buffer = new byte[1024];
MessageDigest md5Hash = MessageDigest.getInstance("MD5");
int numRead = 0;
while (numRead != -1) {
numRead = input.read(buffer);
if (numRead > 0) {
md5Hash.update(buffer, 0, numRead);
}
}
input.close();
byte [] md5Bytes = md5Hash.digest();
for (int i=0; i < md5Bytes.length; i++) {
returnVal += Integer.toString((md5Bytes[i] & 0xff) + 0x100, 16).substring(1);
}
return returnVal;
}
public static String getMD5OfString(String str) throws NoSuchAlgorithmException {
MessageDigest md5;
StringBuffer hexString = new StringBuffer();
md5 = MessageDigest.getInstance("md5");
md5.reset();
md5.update(str.getBytes());
byte messageDigest[] = md5.digest();
for (int i = 0; i < messageDigest.length; i++) {
hexString.append(Integer.toHexString((0xF0 & messageDigest[i])>>4));
hexString.append(Integer.toHexString(0x0F & messageDigest[i]));
}
return hexString.toString();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment