| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668 |
- // Better Streaming Assets, Piotr Gwiazdowski <gwiazdorrr+github at gmail.com>, 2017
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using UnityEngine;
- using System.IO;
- using System.Text.RegularExpressions;
- using Better;
- using Better.StreamingAssets;
- using Better.StreamingAssets.ZipArchive;
- #if UNITY_EDITOR
- using BetterStreamingAssetsImp = BetterStreamingAssets.EditorImpl;
- #elif UNITY_ANDROID
- using BetterStreamingAssetsImp = BetterStreamingAssets.ApkImpl;
- #else
- using BetterStreamingAssetsImp = BetterStreamingAssets.LooseFilesImpl;
- #endif
- public static partial class BetterStreamingAssets
- {
- internal struct ReadInfo
- {
- public string readPath;
- public long size;
- public long offset;
- public uint crc32;
- }
- public static string Root
- {
- get { return BetterStreamingAssetsImp.s_root; }
- }
- public static void Initialize()
- {
- BetterStreamingAssetsImp.Initialize(Application.dataPath, Application.streamingAssetsPath);
- }
- /// <summary>
- /// Android only: raised when there's a Streaming Asset that is compressed. If there is no handler
- /// or it returns false, a warning will be logged.
- /// </summary>
- public static event Func<string, bool> CompressedStreamingAssetFound;
- #if UNITY_EDITOR
- public static void InitializeWithExternalApk(string apkPath)
- {
- BetterStreamingAssetsImp.ApkMode = true;
- BetterStreamingAssetsImp.Initialize(apkPath, "jar:file://" + apkPath + "!/assets/");
- }
- public static void InitializeWithExternalDirectories(string dataPath, string streamingAssetsPath)
- {
- BetterStreamingAssetsImp.ApkMode = false;
- BetterStreamingAssetsImp.Initialize(dataPath, streamingAssetsPath);
- }
- #endif
- public static bool FileExists(string path)
- {
- ReadInfo info;
- return BetterStreamingAssetsImp.TryGetInfo(path, out info);
- }
- public static bool DirectoryExists(string path)
- {
- return BetterStreamingAssetsImp.DirectoryExists(path);
- }
- public static AssetBundleCreateRequest LoadAssetBundleAsync(string path, uint crc = 0)
- {
- var info = GetInfoOrThrow(path);
- return AssetBundle.LoadFromFileAsync(info.readPath, crc, (ulong)info.offset);
- }
- public static AssetBundle LoadAssetBundle(string path, uint crc = 0)
- {
- var info = GetInfoOrThrow(path);
- return AssetBundle.LoadFromFile(info.readPath, crc, (ulong)info.offset);
- }
- public static System.IO.Stream OpenRead(string path)
- {
- if ( path == null )
- throw new ArgumentNullException("path");
- if ( path.Length == 0 )
- throw new ArgumentException("Empty path", "path");
- return BetterStreamingAssetsImp.OpenRead(path);
- }
- public static System.IO.StreamReader OpenText(string path)
- {
- Stream str = OpenRead(path);
- try
- {
- return new StreamReader(str);
- }
- catch (System.Exception)
- {
- if (str != null)
- str.Dispose();
- throw;
- }
- }
- public static string ReadAllText(string path)
- {
- using ( var sr = OpenText(path) )
- {
- return sr.ReadToEnd();
- }
- }
- public static string[] ReadAllLines(string path)
- {
- string line;
- var lines = new List<string>();
- using ( var sr = OpenText(path) )
- {
- while ( ( line = sr.ReadLine() ) != null )
- {
- lines.Add(line);
- }
- }
- return lines.ToArray();
- }
- public static byte[] ReadAllBytes(string path)
- {
- if ( path == null )
- throw new ArgumentNullException("path");
- if ( path.Length == 0 )
- throw new ArgumentException("Empty path", "path");
- return BetterStreamingAssetsImp.ReadAllBytes(path);
- }
- public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
- {
- return BetterStreamingAssetsImp.GetFiles(path, searchPattern, searchOption);
- }
- public static string[] GetFiles(string path)
- {
- return GetFiles(path, null);
- }
- public static string[] GetFiles(string path, string searchPattern)
- {
- return GetFiles(path, searchPattern, SearchOption.TopDirectoryOnly);
- }
- private static ReadInfo GetInfoOrThrow(string path)
- {
- ReadInfo result;
- if ( !BetterStreamingAssetsImp.TryGetInfo(path, out result) )
- ThrowFileNotFound(path);
- return result;
- }
- private static void ThrowFileNotFound(string path)
- {
- throw new FileNotFoundException("File not found", path);
- }
- static partial void AndroidIsCompressedFileStreamingAsset(string path, ref bool result);
- #if UNITY_EDITOR
- internal static class EditorImpl
- {
- public static bool ApkMode = false;
- public static string s_root
- {
- get { return ApkMode ? ApkImpl.s_root : LooseFilesImpl.s_root; }
- }
- internal static void Initialize(string dataPath, string streamingAssetsPath)
- {
- if ( ApkMode )
- {
- ApkImpl.Initialize(dataPath, streamingAssetsPath);
- }
- else
- {
- LooseFilesImpl.Initialize(dataPath, streamingAssetsPath);
- }
- }
- internal static bool TryGetInfo(string path, out ReadInfo info)
- {
- if ( ApkMode )
- return ApkImpl.TryGetInfo(path, out info);
- else
- return LooseFilesImpl.TryGetInfo(path, out info);
- }
- internal static bool DirectoryExists(string path)
- {
- if ( ApkMode )
- return ApkImpl.DirectoryExists(path);
- else
- return LooseFilesImpl.DirectoryExists(path);
- }
- internal static Stream OpenRead(string path)
- {
- if ( ApkMode )
- return ApkImpl.OpenRead(path);
- else
- return LooseFilesImpl.OpenRead(path);
- }
- internal static byte[] ReadAllBytes(string path)
- {
- if ( ApkMode )
- return ApkImpl.ReadAllBytes(path);
- else
- return LooseFilesImpl.ReadAllBytes(path);
- }
- internal static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
- {
- if ( ApkMode )
- return ApkImpl.GetFiles(path, searchPattern, searchOption);
- else
- return LooseFilesImpl.GetFiles(path, searchPattern, searchOption);
- }
- }
- #endif
- #if UNITY_EDITOR || !UNITY_ANDROID
- internal static class LooseFilesImpl
- {
- public static string s_root;
- private static string[] s_emptyArray = new string[0];
- public static void Initialize(string dataPath, string streamingAssetsPath)
- {
- s_root = Path.GetFullPath(streamingAssetsPath).Replace('\\', '/').TrimEnd('/');
- }
- public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
- {
- if (!Directory.Exists(s_root))
- return s_emptyArray;
- // this will throw if something is fishy
- path = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
- Debug.Assert(s_root.Last() != '\\' && s_root.Last() != '/' && path.StartsWith("/"));
- var files = Directory.GetFiles(s_root + path, searchPattern ?? "*", searchOption);
- for ( int i = 0; i < files.Length; ++i )
- {
- Debug.Assert(files[i].StartsWith(s_root));
- files[i] = files[i].Substring(s_root.Length + 1).Replace('\\', '/');
- }
- #if UNITY_EDITOR
- // purge meta files
- {
- int j = 0;
- for ( int i = 0; i < files.Length; ++i )
- {
- if ( !files[i].EndsWith(".meta") )
- {
- files[j++] = files[i];
- }
- }
- Array.Resize(ref files, j);
- }
- #endif
- return files;
- }
- public static bool TryGetInfo(string path, out ReadInfo info)
- {
- path = PathUtil.NormalizeRelativePath(path);
- info = new ReadInfo();
- var fullPath = s_root + path;
- if ( !File.Exists(fullPath) )
- return false;
- info.readPath = fullPath;
- return true;
- }
- public static bool DirectoryExists(string path)
- {
- var normalized = PathUtil.NormalizeRelativePath(path);
- return Directory.Exists(s_root + normalized);
- }
- public static byte[] ReadAllBytes(string path)
- {
- ReadInfo info;
- if ( !TryGetInfo(path, out info) )
- ThrowFileNotFound(path);
- return File.ReadAllBytes(info.readPath);
- }
- public static System.IO.Stream OpenRead(string path)
- {
- ReadInfo info;
- if ( !TryGetInfo(path, out info) )
- ThrowFileNotFound(path);
- Stream fileStream = File.OpenRead(info.readPath);
- try
- {
- return new SubReadOnlyStream(fileStream, leaveOpen: false);
- }
- catch ( System.Exception )
- {
- fileStream.Dispose();
- throw;
- }
- }
- }
- #endif
- #if UNITY_EDITOR || UNITY_ANDROID
- internal static class ApkImpl
- {
- private static string[] s_paths;
- private static PartInfo[] s_streamingAssets;
- public static string s_root;
- private struct PartInfo
- {
- public long size;
- public long offset;
- public uint crc32;
- }
- public static void Initialize(string dataPath, string streamingAssetsPath)
- {
- s_root = dataPath;
- List<string> paths = new List<string>();
- List<PartInfo> parts = new List<PartInfo>();
- GetStreamingAssetsInfoFromJar(s_root, paths, parts);
- if (paths.Count == 0 && !Application.isEditor && Path.GetFileName(dataPath) != "base.apk")
- {
- // maybe split?
- var newDataPath = Path.GetDirectoryName(dataPath) + "/base.apk";
- if (File.Exists(newDataPath))
- {
- s_root = newDataPath;
- GetStreamingAssetsInfoFromJar(newDataPath, paths, parts);
- }
- }
- s_paths = paths.ToArray();
- s_streamingAssets = parts.ToArray();
- }
- public static bool TryGetInfo(string path, out ReadInfo info)
- {
- path = PathUtil.NormalizeRelativePath(path);
- info = new ReadInfo();
- var index = Array.BinarySearch(s_paths, path, StringComparer.OrdinalIgnoreCase);
- if ( index < 0 )
- return false;
- var dataInfo = s_streamingAssets[index];
- info.crc32 = dataInfo.crc32;
- info.offset = dataInfo.offset;
- info.size = dataInfo.size;
- info.readPath = s_root;
- return true;
- }
- public static bool DirectoryExists(string path)
- {
- var normalized = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
- var dirIndex = GetDirectoryIndex(normalized);
- return dirIndex >= 0 && dirIndex < s_paths.Length;
- }
- public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
- {
- if ( path == null )
- throw new ArgumentNullException("path");
- var actualDirPath = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
- // find first file there
- var index = GetDirectoryIndex(actualDirPath);
- if ( index < 0 )
- throw new IOException();
- if ( index == s_paths.Length )
- throw new DirectoryNotFoundException();
- Predicate<string> filter;
- if ( string.IsNullOrEmpty(searchPattern) || searchPattern == "*" )
- {
- filter = null;
- }
- else if ( searchPattern.IndexOf('*') >= 0 || searchPattern.IndexOf('?') >= 0 )
- {
- var regex = PathUtil.WildcardToRegex(searchPattern);
- filter = (x) => regex.IsMatch(x);
- }
- else
- {
- filter = (x) => string.Compare(x, searchPattern, true) == 0;
- }
- List<string> results = new List<string>();
- for ( int i = index; i < s_paths.Length; ++i )
- {
- var filePath = s_paths[i];
- if ( !filePath.StartsWith(actualDirPath) )
- break;
- string fileName;
- var dirSeparatorIndex = filePath.LastIndexOf('/', filePath.Length - 1, filePath.Length - actualDirPath.Length);
- if ( dirSeparatorIndex >= 0 )
- {
- if ( searchOption == SearchOption.TopDirectoryOnly )
- continue;
- fileName = filePath.Substring(dirSeparatorIndex + 1);
- }
- else
- {
- fileName = filePath.Substring(actualDirPath.Length);
- }
- // now do a match
- if ( filter == null || filter(fileName) )
- {
- Debug.Assert(filePath[0] == '/');
- results.Add(filePath.Substring(1));
- }
- }
- return results.ToArray();
- }
- public static byte[] ReadAllBytes(string path)
- {
- ReadInfo info;
- if ( !TryGetInfo(path, out info) )
- ThrowFileNotFound(path);
- byte[] buffer;
- using ( var fileStream = File.OpenRead(info.readPath) )
- {
- if ( info.offset != 0 )
- {
- if ( fileStream.Seek(info.offset, SeekOrigin.Begin) != info.offset )
- throw new IOException();
- }
- if ( info.size > (long)int.MaxValue )
- throw new IOException();
- int count = (int)info.size;
- int offset = 0;
- buffer = new byte[count];
- while ( count > 0 )
- {
- int num = fileStream.Read(buffer, offset, count);
- if ( num == 0 )
- throw new EndOfStreamException();
- offset += num;
- count -= num;
- }
- }
- return buffer;
- }
- public static System.IO.Stream OpenRead(string path)
- {
- ReadInfo info;
- if ( !TryGetInfo(path, out info) )
- ThrowFileNotFound(path);
- Stream fileStream = File.OpenRead(info.readPath);
- try
- {
- return new SubReadOnlyStream(fileStream, info.offset, info.size, leaveOpen : false);
- }
- catch ( System.Exception )
- {
- fileStream.Dispose();
- throw;
- }
- }
- private static int GetDirectoryIndex(string path)
- {
- Debug.Assert(s_paths != null);
- // find first file there
- var index = Array.BinarySearch(s_paths, path, StringComparer.OrdinalIgnoreCase);
- if ( index >= 0 )
- return ~index;
- // if the end, no such directory exists
- index = ~index;
- if ( index == s_paths.Length )
- return index;
- for ( int i = index; i < s_paths.Length && s_paths[i].StartsWith(path); ++i )
- {
- // because otherwise there would be a match
- Debug.Assert(s_paths[i].Length > path.Length);
- if ( path[path.Length - 1] == '/' )
- return i;
- if ( s_paths[i][path.Length] == '/' )
- return i;
- }
- return s_paths.Length;
- }
- private static void GetStreamingAssetsInfoFromJar(string apkPath, List<string> paths, List<PartInfo> parts)
- {
- using ( var stream = File.OpenRead(apkPath) )
- using ( var reader = new BinaryReader(stream) )
- {
- if ( !stream.CanRead )
- throw new ArgumentException();
- if ( !stream.CanSeek )
- throw new ArgumentException();
- long expectedNumberOfEntries;
- long centralDirectoryStart;
- ZipArchiveUtils.ReadEndOfCentralDirectory(stream, reader, out expectedNumberOfEntries, out centralDirectoryStart);
- try
- {
- stream.Seek(centralDirectoryStart, SeekOrigin.Begin);
- long numberOfEntries = 0;
- ZipCentralDirectoryFileHeader header;
- const int prefixLength = 7;
- const string prefix = "assets/";
- const string assetsPrefix = "assets/bin/";
- Debug.Assert(prefixLength == prefix.Length);
- while ( ZipCentralDirectoryFileHeader.TryReadBlock(reader, out header) )
- {
- if ( header.CompressedSize != header.UncompressedSize )
- {
- #if UNITY_ASSERTIONS
- var fileName = Encoding.UTF8.GetString(header.Filename);
- if (fileName.StartsWith(prefix) && !fileName.StartsWith(assetsPrefix))
- {
- bool isStreamingAsset = true;
- AndroidIsCompressedFileStreamingAsset(fileName, ref isStreamingAsset);
- if (!isStreamingAsset)
- {
- // partial method ignored it
- }
- else if (CompressedStreamingAssetFound?.Invoke(fileName) == true)
- {
- // handler ignored it
- }
- else
- {
- Debug.LogAssertionFormat($"BetterStreamingAssets: file {fileName} is where Streaming Assets are put, but is compressed. " +
- $"If this is a App Bundle build, see README.md for a possible workaround. " +
- $"If this file is not a Streaming Asset (has been on purpose by hand or by another plug-in), handle " +
- $"{nameof(CompressedStreamingAssetFound)} event or implement " +
- $"{nameof(AndroidIsCompressedFileStreamingAsset)} partial method to prevent " +
- $"this message from appearing again. ");
- }
- }
- #endif
- // we only want uncompressed files
- }
- else
- {
- var fileName = Encoding.UTF8.GetString(header.Filename);
-
- if (fileName.EndsWith("/"))
- {
- // there's some strangeness when it comes to OBB: directories are listed as files
- // simply ignoring them should be enough
- Debug.Assert(header.UncompressedSize == 0);
- }
- else if ( fileName.StartsWith(prefix) )
- {
- // ignore normal assets...
- if ( fileName.StartsWith(assetsPrefix) )
- {
- // Note: if you put bin directory in your StreamingAssets you will get false negative here
- }
- else
- {
- var relativePath = fileName.Substring(prefixLength - 1);
- var entry = new PartInfo()
- {
- crc32 = header.Crc32,
- offset = header.RelativeOffsetOfLocalHeader, // this offset will need fixing later on
- size = header.UncompressedSize
- };
- var index = paths.BinarySearch(relativePath, StringComparer.OrdinalIgnoreCase);
- if ( index >= 0 )
- throw new System.InvalidOperationException("Paths duplicate! " + fileName);
- paths.Insert(~index, relativePath);
- parts.Insert(~index, entry);
- }
- }
- }
- numberOfEntries++;
- }
- if ( numberOfEntries != expectedNumberOfEntries )
- throw new ZipArchiveException("Number of entries does not match");
- }
- catch ( EndOfStreamException ex )
- {
- throw new ZipArchiveException("CentralDirectoryInvalid", ex);
- }
- // now fix offsets
- for ( int i = 0; i < parts.Count; ++i )
- {
- var entry = parts[i];
- stream.Seek(entry.offset, SeekOrigin.Begin);
- if ( !ZipLocalFileHeader.TrySkipBlock(reader) )
- throw new ZipArchiveException("Local file header corrupt");
- entry.offset = stream.Position;
- parts[i] = entry;
- }
- }
- }
- }
- #endif
- }
|