BSA_BetterStreamingAssets.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. // Better Streaming Assets, Piotr Gwiazdowski <gwiazdorrr+github at gmail.com>, 2017
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using UnityEngine;
  7. using System.IO;
  8. using System.Text.RegularExpressions;
  9. using Better;
  10. using Better.StreamingAssets;
  11. using Better.StreamingAssets.ZipArchive;
  12. #if UNITY_EDITOR
  13. using BetterStreamingAssetsImp = BetterStreamingAssets.EditorImpl;
  14. #elif UNITY_ANDROID
  15. using BetterStreamingAssetsImp = BetterStreamingAssets.ApkImpl;
  16. #else
  17. using BetterStreamingAssetsImp = BetterStreamingAssets.LooseFilesImpl;
  18. #endif
  19. public static class BetterStreamingAssets
  20. {
  21. internal struct ReadInfo
  22. {
  23. public string readPath;
  24. public long size;
  25. public long offset;
  26. public uint crc32;
  27. }
  28. public static string Root
  29. {
  30. get { return BetterStreamingAssetsImp.s_root; }
  31. }
  32. public static void Initialize()
  33. {
  34. BetterStreamingAssetsImp.Initialize(Application.dataPath, Application.streamingAssetsPath);
  35. }
  36. #if UNITY_EDITOR
  37. public static void InitializeWithExternalApk(string apkPath)
  38. {
  39. BetterStreamingAssetsImp.ApkMode = true;
  40. BetterStreamingAssetsImp.Initialize(apkPath, "jar:file://" + apkPath + "!/assets/");
  41. }
  42. public static void InitializeWithExternalDirectories(string dataPath, string streamingAssetsPath)
  43. {
  44. BetterStreamingAssetsImp.ApkMode = false;
  45. BetterStreamingAssetsImp.Initialize(dataPath, streamingAssetsPath);
  46. }
  47. #endif
  48. public static bool FileExists(string path)
  49. {
  50. ReadInfo info;
  51. return BetterStreamingAssetsImp.TryGetInfo(path, out info);
  52. }
  53. public static bool DirectoryExists(string path)
  54. {
  55. return BetterStreamingAssetsImp.DirectoryExists(path);
  56. }
  57. public static AssetBundleCreateRequest LoadAssetBundleAsync(string path, uint crc = 0)
  58. {
  59. var info = GetInfoOrThrow(path);
  60. return AssetBundle.LoadFromFileAsync(info.readPath, crc, (ulong)info.offset);
  61. }
  62. public static AssetBundle LoadAssetBundle(string path, uint crc = 0)
  63. {
  64. var info = GetInfoOrThrow(path);
  65. return AssetBundle.LoadFromFile(info.readPath, crc, (ulong)info.offset);
  66. }
  67. public static System.IO.Stream OpenRead(string path)
  68. {
  69. if ( path == null )
  70. throw new ArgumentNullException("path");
  71. if ( path.Length == 0 )
  72. throw new ArgumentException("Empty path", "path");
  73. return BetterStreamingAssetsImp.OpenRead(path);
  74. }
  75. public static System.IO.StreamReader OpenText(string path)
  76. {
  77. Stream str = OpenRead(path);
  78. try
  79. {
  80. return new StreamReader(str);
  81. }
  82. catch (System.Exception)
  83. {
  84. if (str != null)
  85. str.Dispose();
  86. throw;
  87. }
  88. }
  89. public static string ReadAllText(string path)
  90. {
  91. using ( var sr = OpenText(path) )
  92. {
  93. return sr.ReadToEnd();
  94. }
  95. }
  96. public static string[] ReadAllLines(string path)
  97. {
  98. string line;
  99. var lines = new List<string>();
  100. using ( var sr = OpenText(path) )
  101. {
  102. while ( ( line = sr.ReadLine() ) != null )
  103. {
  104. lines.Add(line);
  105. }
  106. }
  107. return lines.ToArray();
  108. }
  109. public static byte[] ReadAllBytes(string path)
  110. {
  111. if ( path == null )
  112. throw new ArgumentNullException("path");
  113. if ( path.Length == 0 )
  114. throw new ArgumentException("Empty path", "path");
  115. return BetterStreamingAssetsImp.ReadAllBytes(path);
  116. }
  117. public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
  118. {
  119. return BetterStreamingAssetsImp.GetFiles(path, searchPattern, searchOption);
  120. }
  121. public static string[] GetFiles(string path)
  122. {
  123. return GetFiles(path, null);
  124. }
  125. public static string[] GetFiles(string path, string searchPattern)
  126. {
  127. return GetFiles(path, searchPattern, SearchOption.TopDirectoryOnly);
  128. }
  129. private static ReadInfo GetInfoOrThrow(string path)
  130. {
  131. ReadInfo result;
  132. if ( !BetterStreamingAssetsImp.TryGetInfo(path, out result) )
  133. ThrowFileNotFound(path);
  134. return result;
  135. }
  136. private static void ThrowFileNotFound(string path)
  137. {
  138. throw new FileNotFoundException("File not found", path);
  139. }
  140. #if UNITY_EDITOR
  141. internal static class EditorImpl
  142. {
  143. public static bool ApkMode = false;
  144. public static string s_root
  145. {
  146. get { return ApkMode ? ApkImpl.s_root : LooseFilesImpl.s_root; }
  147. }
  148. internal static void Initialize(string dataPath, string streamingAssetsPath)
  149. {
  150. if ( ApkMode )
  151. {
  152. ApkImpl.Initialize(dataPath, streamingAssetsPath);
  153. }
  154. else
  155. {
  156. LooseFilesImpl.Initialize(dataPath, streamingAssetsPath);
  157. }
  158. }
  159. internal static bool TryGetInfo(string path, out ReadInfo info)
  160. {
  161. if ( ApkMode )
  162. return ApkImpl.TryGetInfo(path, out info);
  163. else
  164. return LooseFilesImpl.TryGetInfo(path, out info);
  165. }
  166. internal static bool DirectoryExists(string path)
  167. {
  168. if ( ApkMode )
  169. return ApkImpl.DirectoryExists(path);
  170. else
  171. return LooseFilesImpl.DirectoryExists(path);
  172. }
  173. internal static Stream OpenRead(string path)
  174. {
  175. if ( ApkMode )
  176. return ApkImpl.OpenRead(path);
  177. else
  178. return LooseFilesImpl.OpenRead(path);
  179. }
  180. internal static byte[] ReadAllBytes(string path)
  181. {
  182. if ( ApkMode )
  183. return ApkImpl.ReadAllBytes(path);
  184. else
  185. return LooseFilesImpl.ReadAllBytes(path);
  186. }
  187. internal static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
  188. {
  189. if ( ApkMode )
  190. return ApkImpl.GetFiles(path, searchPattern, searchOption);
  191. else
  192. return LooseFilesImpl.GetFiles(path, searchPattern, searchOption);
  193. }
  194. }
  195. #endif
  196. #if UNITY_EDITOR || !UNITY_ANDROID
  197. internal static class LooseFilesImpl
  198. {
  199. public static string s_root;
  200. private static string[] s_emptyArray = new string[0];
  201. public static void Initialize(string dataPath, string streamingAssetsPath)
  202. {
  203. s_root = Path.GetFullPath(streamingAssetsPath).Replace('\\', '/').TrimEnd('/');
  204. }
  205. public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
  206. {
  207. if (!Directory.Exists(s_root))
  208. return s_emptyArray;
  209. // this will throw if something is fishy
  210. path = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
  211. Debug.Assert(s_root.Last() != '\\' && s_root.Last() != '/' && path.StartsWith("/"));
  212. var files = Directory.GetFiles(s_root + path, searchPattern ?? "*", searchOption);
  213. for ( int i = 0; i < files.Length; ++i )
  214. {
  215. Debug.Assert(files[i].StartsWith(s_root));
  216. files[i] = files[i].Substring(s_root.Length + 1).Replace('\\', '/');
  217. }
  218. #if UNITY_EDITOR
  219. // purge meta files
  220. {
  221. int j = 0;
  222. for ( int i = 0; i < files.Length; ++i )
  223. {
  224. if ( !files[i].EndsWith(".meta") )
  225. {
  226. files[j++] = files[i];
  227. }
  228. }
  229. Array.Resize(ref files, j);
  230. }
  231. #endif
  232. return files;
  233. }
  234. public static bool TryGetInfo(string path, out ReadInfo info)
  235. {
  236. path = PathUtil.NormalizeRelativePath(path);
  237. info = new ReadInfo();
  238. var fullPath = s_root + path;
  239. if ( !File.Exists(fullPath) )
  240. return false;
  241. info.readPath = fullPath;
  242. return true;
  243. }
  244. public static bool DirectoryExists(string path)
  245. {
  246. var normalized = PathUtil.NormalizeRelativePath(path);
  247. return Directory.Exists(s_root + normalized);
  248. }
  249. public static byte[] ReadAllBytes(string path)
  250. {
  251. ReadInfo info;
  252. if ( !TryGetInfo(path, out info) )
  253. ThrowFileNotFound(path);
  254. return File.ReadAllBytes(info.readPath);
  255. }
  256. public static System.IO.Stream OpenRead(string path)
  257. {
  258. ReadInfo info;
  259. if ( !TryGetInfo(path, out info) )
  260. ThrowFileNotFound(path);
  261. Stream fileStream = File.OpenRead(info.readPath);
  262. try
  263. {
  264. return new SubReadOnlyStream(fileStream, leaveOpen: false);
  265. }
  266. catch ( System.Exception )
  267. {
  268. fileStream.Dispose();
  269. throw;
  270. }
  271. }
  272. }
  273. #endif
  274. #if UNITY_EDITOR || UNITY_ANDROID
  275. internal static class ApkImpl
  276. {
  277. private static string[] s_paths;
  278. private static PartInfo[] s_streamingAssets;
  279. public static string s_root;
  280. private struct PartInfo
  281. {
  282. public long size;
  283. public long offset;
  284. public uint crc32;
  285. }
  286. public static void Initialize(string dataPath, string streamingAssetsPath)
  287. {
  288. s_root = dataPath;
  289. List<string> paths = new List<string>();
  290. List<PartInfo> parts = new List<PartInfo>();
  291. GetStreamingAssetsInfoFromJar(s_root, paths, parts);
  292. if (paths.Count == 0 && !Application.isEditor && Path.GetFileName(dataPath) != "base.apk")
  293. {
  294. // maybe split?
  295. var newDataPath = Path.GetDirectoryName(dataPath) + "/base.apk";
  296. if (File.Exists(newDataPath))
  297. {
  298. s_root = newDataPath;
  299. GetStreamingAssetsInfoFromJar(newDataPath, paths, parts);
  300. }
  301. }
  302. s_paths = paths.ToArray();
  303. s_streamingAssets = parts.ToArray();
  304. }
  305. public static bool TryGetInfo(string path, out ReadInfo info)
  306. {
  307. path = PathUtil.NormalizeRelativePath(path);
  308. info = new ReadInfo();
  309. var index = Array.BinarySearch(s_paths, path, StringComparer.OrdinalIgnoreCase);
  310. if ( index < 0 )
  311. return false;
  312. var dataInfo = s_streamingAssets[index];
  313. info.crc32 = dataInfo.crc32;
  314. info.offset = dataInfo.offset;
  315. info.size = dataInfo.size;
  316. info.readPath = s_root;
  317. return true;
  318. }
  319. public static bool DirectoryExists(string path)
  320. {
  321. var normalized = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
  322. var dirIndex = GetDirectoryIndex(normalized);
  323. return dirIndex >= 0 && dirIndex < s_paths.Length;
  324. }
  325. public static string[] GetFiles(string path, string searchPattern, SearchOption searchOption)
  326. {
  327. if ( path == null )
  328. throw new ArgumentNullException("path");
  329. var actualDirPath = PathUtil.NormalizeRelativePath(path, forceTrailingSlash : true);
  330. // find first file there
  331. var index = GetDirectoryIndex(actualDirPath);
  332. if ( index < 0 )
  333. throw new IOException();
  334. if ( index == s_paths.Length )
  335. throw new DirectoryNotFoundException();
  336. Predicate<string> filter;
  337. if ( string.IsNullOrEmpty(searchPattern) || searchPattern == "*" )
  338. {
  339. filter = null;
  340. }
  341. else if ( searchPattern.IndexOf('*') >= 0 || searchPattern.IndexOf('?') >= 0 )
  342. {
  343. var regex = PathUtil.WildcardToRegex(searchPattern);
  344. filter = (x) => regex.IsMatch(x);
  345. }
  346. else
  347. {
  348. filter = (x) => string.Compare(x, searchPattern, true) == 0;
  349. }
  350. List<string> results = new List<string>();
  351. string fixedPath = null;
  352. for ( int i = index; i < s_paths.Length; ++i )
  353. {
  354. var filePath = s_paths[i];
  355. if ( !filePath.StartsWith(actualDirPath) )
  356. break;
  357. string fileName;
  358. var dirSeparatorIndex = filePath.LastIndexOf('/', filePath.Length - 1, filePath.Length - actualDirPath.Length);
  359. if ( dirSeparatorIndex >= 0 )
  360. {
  361. if ( searchOption == SearchOption.TopDirectoryOnly )
  362. continue;
  363. fileName = filePath.Substring(dirSeparatorIndex + 1);
  364. }
  365. else
  366. {
  367. fileName = filePath.Substring(actualDirPath.Length);
  368. }
  369. // now do a match
  370. if ( filter == null || filter(fileName) )
  371. {
  372. var normalizedPart = filePath.Substring(actualDirPath.Length);
  373. if ( fixedPath == null )
  374. {
  375. fixedPath = PathUtil.FixTrailingDirectorySeparators(path);
  376. if ( fixedPath == "/" )
  377. fixedPath = string.Empty;
  378. }
  379. var result = PathUtil.CombineSlash(fixedPath, normalizedPart);
  380. results.Add(result);
  381. }
  382. }
  383. return results.ToArray();
  384. }
  385. public static byte[] ReadAllBytes(string path)
  386. {
  387. ReadInfo info;
  388. if ( !TryGetInfo(path, out info) )
  389. ThrowFileNotFound(path);
  390. byte[] buffer;
  391. using ( var fileStream = File.OpenRead(info.readPath) )
  392. {
  393. if ( info.offset != 0 )
  394. {
  395. if ( fileStream.Seek(info.offset, SeekOrigin.Begin) != info.offset )
  396. throw new IOException();
  397. }
  398. if ( info.size > (long)int.MaxValue )
  399. throw new IOException();
  400. int count = (int)info.size;
  401. int offset = 0;
  402. buffer = new byte[count];
  403. while ( count > 0 )
  404. {
  405. int num = fileStream.Read(buffer, offset, count);
  406. if ( num == 0 )
  407. throw new EndOfStreamException();
  408. offset += num;
  409. count -= num;
  410. }
  411. }
  412. return buffer;
  413. }
  414. public static System.IO.Stream OpenRead(string path)
  415. {
  416. ReadInfo info;
  417. if ( !TryGetInfo(path, out info) )
  418. ThrowFileNotFound(path);
  419. Stream fileStream = File.OpenRead(info.readPath);
  420. try
  421. {
  422. return new SubReadOnlyStream(fileStream, info.offset, info.size, leaveOpen : false);
  423. }
  424. catch ( System.Exception )
  425. {
  426. fileStream.Dispose();
  427. throw;
  428. }
  429. }
  430. private static int GetDirectoryIndex(string path)
  431. {
  432. Debug.Assert(s_paths != null);
  433. // find first file there
  434. var index = Array.BinarySearch(s_paths, path, StringComparer.OrdinalIgnoreCase);
  435. if ( index >= 0 )
  436. return ~index;
  437. // if the end, no such directory exists
  438. index = ~index;
  439. if ( index == s_paths.Length )
  440. return index;
  441. for ( int i = index; i < s_paths.Length && s_paths[i].StartsWith(path); ++i )
  442. {
  443. // because otherwise there would be a match
  444. Debug.Assert(s_paths[i].Length > path.Length);
  445. if ( path[path.Length - 1] == '/' )
  446. return i;
  447. if ( s_paths[i][path.Length] == '/' )
  448. return i;
  449. }
  450. return s_paths.Length;
  451. }
  452. private static void GetStreamingAssetsInfoFromJar(string apkPath, List<string> paths, List<PartInfo> parts)
  453. {
  454. using ( var stream = File.OpenRead(apkPath) )
  455. using ( var reader = new BinaryReader(stream) )
  456. {
  457. if ( !stream.CanRead )
  458. throw new ArgumentException();
  459. if ( !stream.CanSeek )
  460. throw new ArgumentException();
  461. long expectedNumberOfEntries;
  462. long centralDirectoryStart;
  463. ZipArchiveUtils.ReadEndOfCentralDirectory(stream, reader, out expectedNumberOfEntries, out centralDirectoryStart);
  464. try
  465. {
  466. stream.Seek(centralDirectoryStart, SeekOrigin.Begin);
  467. long numberOfEntries = 0;
  468. ZipCentralDirectoryFileHeader header;
  469. const int prefixLength = 7;
  470. const string prefix = "assets/";
  471. const string assetsPrefix = "assets/bin/";
  472. Debug.Assert(prefixLength == prefix.Length);
  473. while ( ZipCentralDirectoryFileHeader.TryReadBlock(reader, out header) )
  474. {
  475. if ( header.CompressedSize != header.UncompressedSize )
  476. {
  477. #if UNITY_ASSERTIONS
  478. var fileName = Encoding.UTF8.GetString(header.Filename);
  479. if (fileName.StartsWith(prefix) && !fileName.StartsWith(assetsPrefix))
  480. {
  481. 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);
  482. }
  483. #endif
  484. // we only want uncompressed files
  485. }
  486. else
  487. {
  488. var fileName = Encoding.UTF8.GetString(header.Filename);
  489. if (fileName.EndsWith("/"))
  490. {
  491. // there's some strangeness when it comes to OBB: directories are listed as files
  492. // simply ignoring them should be enough
  493. Debug.Assert(header.UncompressedSize == 0);
  494. }
  495. else if ( fileName.StartsWith(prefix) )
  496. {
  497. // ignore normal assets...
  498. if ( fileName.StartsWith(assetsPrefix) )
  499. {
  500. // Note: if you put bin directory in your StreamingAssets you will get false negative here
  501. }
  502. else
  503. {
  504. var relativePath = fileName.Substring(prefixLength - 1);
  505. var entry = new PartInfo()
  506. {
  507. crc32 = header.Crc32,
  508. offset = header.RelativeOffsetOfLocalHeader, // this offset will need fixing later on
  509. size = header.UncompressedSize
  510. };
  511. var index = paths.BinarySearch(relativePath, StringComparer.OrdinalIgnoreCase);
  512. if ( index >= 0 )
  513. throw new System.InvalidOperationException("Paths duplicate! " + fileName);
  514. paths.Insert(~index, relativePath);
  515. parts.Insert(~index, entry);
  516. }
  517. }
  518. }
  519. numberOfEntries++;
  520. }
  521. if ( numberOfEntries != expectedNumberOfEntries )
  522. throw new ZipArchiveException("Number of entries does not match");
  523. }
  524. catch ( EndOfStreamException ex )
  525. {
  526. throw new ZipArchiveException("CentralDirectoryInvalid", ex);
  527. }
  528. // now fix offsets
  529. for ( int i = 0; i < parts.Count; ++i )
  530. {
  531. var entry = parts[i];
  532. stream.Seek(entry.offset, SeekOrigin.Begin);
  533. if ( !ZipLocalFileHeader.TrySkipBlock(reader) )
  534. throw new ZipArchiveException("Local file header corrupt");
  535. entry.offset = stream.Position;
  536. parts[i] = entry;
  537. }
  538. }
  539. }
  540. }
  541. #endif
  542. }