Skip to content

Case-insensitive ZIP loading #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions polymod/PolymodConfig.hx
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,22 @@ class PolymodConfig
modIgnoreFiles = DefineUtil.getDefineStringArray('POLYMOD_MOD_IGNORE', ['LICENSE.txt', 'ASSET_LICENSE.txt', 'CODE_LICENSE.txt']);
return modIgnoreFiles;
}

/**
* If true, ZIP mods' assets will be loaded in a case-insensitive way.
* For example, if trying to load an asset called `FOO.txt`, a file called `foo.txt` will be loaded if it exists.
*
* Enable this option by setting the `POLYMOD_ZIP_INSENSITIVE` Haxe define at compile time,
* or by setting this value in your code.
*
* @default `false`
*/
public static var caseInsensitiveZipLoading(get, default):Null<Bool>;

static function get_caseInsensitiveZipLoading():Bool
{
if (caseInsensitiveZipLoading == null)
caseInsensitiveZipLoading = DefineUtil.getDefineBool('POLYMOD_ZIP_INSENSITIVE', false);
return caseInsensitiveZipLoading;
}
}
22 changes: 16 additions & 6 deletions polymod/fs/SysZipFileSystem.hx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ class SysZipFileSystem extends polymod.fs.StubFileSystem
}
}
#else
import haxe.Constraints.IMap;
import haxe.ds.StringMap;
import haxe.io.Bytes;
import haxe.io.Path;
import polymod.Polymod.ModMetadata;
import polymod.util.Util;
import polymod.util.InsensitiveMap;
import polymod.util.zip.ZipParser;
import sys.io.File;
import thx.semver.VersionRule;
Expand All @@ -33,7 +36,7 @@ class SysZipFileSystem extends SysFileSystem
/**
* Specifies the name of the ZIP that contains each file.
*/
var filesLocations:Map<String, String>;
var filesLocations:IMap<String, String>;

/**
* Specifies the names of available directories within the ZIP files.
Expand All @@ -48,7 +51,7 @@ class SysZipFileSystem extends SysFileSystem
public function new(params:ZipFileSystemParams)
{
super(params);
filesLocations = new Map<String, String>();
filesLocations = PolymodConfig.caseInsensitiveZipLoading ? new InsensitiveMap() : new StringMap();
zipParsers = new Map<String, ZipParser>();
fileDirectories = [];

Expand Down Expand Up @@ -125,7 +128,7 @@ class SysZipFileSystem extends SysFileSystem
return super.isDirectory(path);
}

public override function readDirectory(path:String)
public override function readDirectory(path:String):Array<String>
{
// Remove trailing slash
if (path.endsWith("/"))
Expand All @@ -136,19 +139,26 @@ class SysZipFileSystem extends SysFileSystem

if (fileDirectories.contains(path))
{
final insensitive:Bool = PolymodConfig.caseInsensitiveZipLoading;
if (insensitive)
path = path.toLowerCase();

// We check if directory ==, because
// we don't want to read the directory recursively.

for (file in filesLocations.keys())
{
if (Path.directory(file) == path)
var filePath = Path.directory(file);
if (insensitive) filePath = filePath.toLowerCase();
if (filePath == path)
{
result.push(Path.withoutDirectory(file));
}
}
for (dir in fileDirectories)
{
if (Path.directory(dir) == path)
var dirPath = Path.directory(dir);
if (insensitive) dirPath = dirPath.toLowerCase();
if (dirPath == path)
{
result.push(Path.withoutDirectory(dir));
}
Expand Down
79 changes: 79 additions & 0 deletions polymod/util/InsensitiveMap.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package polymod.util;

import haxe.ds.StringMap;
import haxe.Constraints.IMap;

/**
* A string map which treats any letter cases the same (case insensitive).
* Unlike other maps, if a value already exists it won't be overwritten.
*/
class InsensitiveMap<T> implements IMap<String, T> {
var data:StringMap<T> = new StringMap();
var originalKeys:Map<String, String> = new Map();

public function new() {}

public function set(key:String, value:T):Void {
var lowerKey = key.toLowerCase();
if (data.exists(lowerKey)) return;

data.set(lowerKey, value);
originalKeys.set(lowerKey, key);
}

public inline function get(key:String):Null<T> {
return data.get(key.toLowerCase());
}

public inline function exists(key:String):Bool {
return data.exists(key.toLowerCase());
}

public function remove(key:String):Bool {
var lowerKey = key.toLowerCase();
originalKeys.remove(lowerKey);
return data.remove(lowerKey);
}

public function clear():Void {
data.clear();
originalKeys.clear();
}

public function copy():InsensitiveMap<T> {
var res = new InsensitiveMap();
res.data = data.copy();
res.originalKeys = originalKeys.copy();
return res;
}

public inline function keys():Iterator<String> {
return originalKeys.iterator();
}

public function keyValueIterator() {
return {
var it = originalKeys.keys();
return {
hasNext: function() return it.hasNext(),
next: function() {
var lowerKey = it.next();
var originalKey = originalKeys.get(lowerKey);
return { key: originalKey, value: data.get(lowerKey) };
}
};
};
}

public inline function iterator() {
return data.iterator();
}

public function toString():String {
var parts = [];
for (key in keys()) {
parts.push('$key => ${get(key)}');
}
return '{' + parts.join(', ') + '}';
}
}
6 changes: 4 additions & 2 deletions polymod/util/zip/ZipParser.hx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package polymod.util.zip;

#if sys
import haxe.Constraints.IMap;
import haxe.ds.StringMap;
import haxe.io.Bytes;
import polymod.util.InsensitiveMap;
import sys.io.File;
import sys.io.FileInput;

Expand Down Expand Up @@ -34,7 +36,7 @@ class ZipParser
* The central directory records, as parsed from the central directory.
* These contain metadata about each file in the archive.
*/
public var centralDirectoryRecords:StringMap<CentralDirectoryFileHeader>;
public var centralDirectoryRecords:IMap<String, CentralDirectoryFileHeader>;

public function new(fileName:String)
{
Expand Down Expand Up @@ -68,7 +70,7 @@ class ZipParser
*/
function getAllCentralDirectoryHeaders():Void
{
this.centralDirectoryRecords = new StringMap();
this.centralDirectoryRecords = PolymodConfig.caseInsensitiveZipLoading ? new InsensitiveMap() : new StringMap();
fileHandle.seek(this.endOfCentralDirectoryRecord.cdrOffset, SeekBegin);
for (_ in 0...this.endOfCentralDirectoryRecord.cdrsTotal)
{
Expand Down