Files
RaindropViewer/Assets/Plugins/BetterStreamingAssets/BSA_BetterStreamingAssets.cs
T
alexiscatnip cdd8470fdf Changed all Bitmap -> Texture2D.
Make demo scene to browse tga files in openmetaverse_data
added BetterStreamingAssets to read from SA
some refactoring.
2021-08-07 00:13:20 +08:00

653 lines
21 KiB
C#

// 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 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);
}
#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);
}
#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>();
string fixedPath = null;
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) )
{
var normalizedPart = filePath.Substring(actualDirPath.Length);
if ( fixedPath == null )
{
fixedPath = PathUtil.FixTrailingDirectorySeparators(path);
if ( fixedPath == "/" )
fixedPath = string.Empty;
}
var result = PathUtil.CombineSlash(fixedPath, normalizedPart);
results.Add(result);
}
}
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))
{
Debug.LogAssertionFormat("BetterStreamingAssets: file {0} seems to be a Streaming Asset, but is compressed. If this is a App Bundle build, see README for a possible workaround.", fileName);
}
#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
}