FileSystem.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. #region License information
  2. // ----------------------------------------------------------------------------
  3. //
  4. // libeq2 - A library for analyzing the Everquest II File Format
  5. // Blaz (blaz@blazlabs.com)
  6. //
  7. // This program is free software; you can redistribute it and/or
  8. // modify it under the terms of the GNU General Public License
  9. // as published by the Free Software Foundation; either version 2
  10. // of the License, or (at your option) any later version.
  11. //
  12. // This program is distributed in the hope that it will be useful,
  13. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. // GNU General Public License for more details.
  16. //
  17. // You should have received a copy of the GNU General Public License
  18. // along with this program; if not, write to the Free Software
  19. // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
  20. //
  21. // ( The full text of the license can be found in the License.txt file )
  22. //
  23. // ----------------------------------------------------------------------------
  24. #endregion
  25. #region Using directives
  26. using System;
  27. using System.Collections.Generic;
  28. using System.Collections.ObjectModel;
  29. using System.Diagnostics;
  30. using ICSharpCode.SharpZipLib;
  31. using ICSharpCode.SharpZipLib.Zip.Compression;
  32. using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
  33. using Sys = System.IO;
  34. #endregion
  35. namespace Everquest2.IO
  36. {
  37. public partial class FileSystem
  38. {
  39. #region Methods
  40. #region Constructors
  41. public FileSystem()
  42. {
  43. }
  44. public FileSystem(string path) : this(path, Sys.FileMode.Open)
  45. {
  46. }
  47. public FileSystem(string path, Sys.FileMode mode) : this(path, mode, Sys.FileAccess.Read)
  48. {
  49. }
  50. public FileSystem(string path, Sys.FileMode mode, Sys.FileAccess access)
  51. {
  52. Open(path, mode, access);
  53. }
  54. public void Open(string path)
  55. {
  56. Open(path, Sys.FileMode.Open);
  57. }
  58. public void Open(string path, Sys.FileMode mode)
  59. {
  60. Open(path, mode, Sys.FileAccess.Read);
  61. }
  62. public void Open(string path, Sys.FileMode mode, Sys.FileAccess access)
  63. {
  64. #region Preconditions
  65. if (path == null) throw new ArgumentNullException("path", "Path must not be null.");
  66. if (mode != Sys.FileMode.Open) throw new NotSupportedException("Only 'open' mode is implemented.");
  67. if (access != Sys.FileAccess.Read) throw new NotSupportedException("Write access is not implemented. Please specify read-only access.");
  68. #endregion
  69. rootDirectory = null;
  70. directories.Clear();
  71. files.Clear();
  72. // Extract the base path from the specified file name. This path will be used to locate the referenced VPK files.
  73. basePath = System.IO.Path.GetDirectoryName(path) + Sys.Path.DirectorySeparatorChar;
  74. if (!Sys.Directory.Exists(basePath)) throw new ArgumentException("The specified path is invalid. The '" + basePath + "' directory doesn't exist.", "path");
  75. // Create root directory.
  76. rootDirectory = new DirectoryInfo(this, string.Empty);
  77. directories.Add(string.Empty, rootDirectory);
  78. OnDirectoryAdded(rootDirectory);
  79. // The System.IO.FileStream constructor may throw the following exceptions:
  80. // - ArgumentOutOfRangeException: mode, access, or share contain an invalid value.
  81. // - FileNotFoundException: The file cannot be found.
  82. // - IOException: An I/O error occurs.
  83. // - SecurityException: The caller does not have the required permission.
  84. // - UnauthorizedAccessException: The access requested is not permitted by the operating system for the specified path.
  85. // None of these exceptions are caught here.
  86. Sys.FileStream stream = new Sys.FileStream(path, mode, access);
  87. Initialize(stream);
  88. }
  89. #endregion
  90. #region Creation methods
  91. public static FileSystem Create(string path)
  92. {
  93. Sys.FileStream stream = new Sys.FileStream(path, Sys.FileMode.Create, Sys.FileAccess.ReadWrite);
  94. return Create(stream);
  95. }
  96. public static FileSystem Create(Sys.Stream stream)
  97. {
  98. #region Preconditions
  99. throw new NotImplementedException("File system creation not supported.");
  100. #endregion
  101. }
  102. #endregion
  103. private void Initialize(Sys.Stream stream)
  104. {
  105. // The System.IO.BinaryReader constructor may throw the following exceptions:
  106. // - ArgumentException: The stream does not support reading, the stream is null, or the stream is already closed.
  107. // This exception is not caught here.
  108. Sys.BinaryReader reader = new Sys.BinaryReader(stream);
  109. // The VplHeader constructor may throw the following exceptions:
  110. // - EndOfStreamException: The end of the stream is reached.
  111. // - ObjectDisposedException: The stream is closed.
  112. // - IOException: An I/O error occurs.
  113. // None of these exceptions are caught here.
  114. header = new VplHeader(reader);
  115. stream.Seek(header.DirectoryOffset, Sys.SeekOrigin.Begin);
  116. VplFileEntry[] directory = new VplFileEntry[header.DirectoryEntryCount];
  117. for (int i = 0; i < directory.Length; ++i) directory[i] = new VplFileEntry(reader);
  118. stream.Seek(header.Unknown0x04, Sys.SeekOrigin.Begin);
  119. uint[] unk4 = new uint[header.DirectoryEntryCount];
  120. for (int i =0; i < header.DirectoryEntryCount; ++i) unk4[i] = reader.ReadUInt32();
  121. VplDirectoryEntry[] dirs = new VplDirectoryEntry[(header.Unknown0x10 - header.Unknown0x08)/ 20];
  122. for (int i = 0; i < (header.Unknown0x10 - header.Unknown0x08)/ 20; ++i) dirs[i] = new VplDirectoryEntry(reader);
  123. // We don't need this reader anymore
  124. reader = null;
  125. // If header.VpkDirectoryEntryCount is zero there are no referenced VPK files in this filesystem.
  126. if (header.VpkDirectoryEntryCount > 0)
  127. {
  128. ProcessVplFile(stream, header);
  129. }
  130. }
  131. private void ProcessVplFile(Sys.Stream stream, VplHeader header)
  132. {
  133. ReadVpkDirectory(stream, header);
  134. ReadFileDirectory(stream, header);
  135. }
  136. private void ReadVpkDirectory(Sys.Stream stream, VplHeader header)
  137. {
  138. InflaterInputStream inflater = new InflaterInputStream(stream);
  139. stream.Seek(header.VpkNamesOffset, Sys.SeekOrigin.Begin);
  140. byte[] rawByteNames = new byte[header.VpkNamesInflatedSize];
  141. // Decompress the referenced VPK file names
  142. // The DeflateStream.Read method may throw the following exceptions:
  143. // - ZipException: Inflater needs a dictionary.
  144. // This exception is rethrown inside an Sys.InvalidDataException exception.
  145. try
  146. {
  147. int bytesRead = 0;
  148. while (inflater.Available == 1 && bytesRead < header.VpkNamesInflatedSize)
  149. {
  150. bytesRead += inflater.Read(rawByteNames, bytesRead, header.VpkNamesInflatedSize - bytesRead);
  151. }
  152. }
  153. catch (ZipException e)
  154. {
  155. throw new Sys.InvalidDataException("Error reading VPK names. The compressed data is invalid.", e);
  156. }
  157. // Transform the bytes to a string. The strings are stored using the ASCII encoding.
  158. Sys.MemoryStream memoryStream = new Sys.MemoryStream(rawByteNames, false);
  159. Sys.BinaryReader reader = new Sys.BinaryReader(memoryStream, System.Text.Encoding.ASCII);
  160. string rawNames = new string(reader.ReadChars(header.VpkNamesInflatedSize));
  161. // The names are stored as a single chunk, separated by null characters.
  162. // Here we chop the string into the individual names.
  163. vpkFiles = rawNames.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
  164. if (vpkFiles.Length != header.VpkDirectoryEntryCount) throw new Sys.InvalidDataException("The number of referenced VPK file names (" + vpkFiles.Length + ") doesn't match the info in the header (" + header.VpkDirectoryEntryCount + ")");
  165. // Convert absolute VPK file paths to relative file paths.
  166. // Note: In some systems this step isn't necessary because the VPK file paths
  167. // are already relative to the base EQ2 installation directory.
  168. for (int i = 0; i < vpkFiles.Length; ++i)
  169. {
  170. string path = vpkFiles[i].Replace('/', Sys.Path.DirectorySeparatorChar);
  171. if (Sys.Path.IsPathRooted(path))
  172. {
  173. if (!Sys.File.Exists(path)) throw new Sys.FileNotFoundException("The referenced VPK file '" + path + "' doesn't exist.");
  174. // Remove the base path from the VPK file path
  175. vpkFiles[i] = path.Remove(0, basePath.Length);
  176. }
  177. else
  178. {
  179. string absolutePath = basePath + path;
  180. if (!Sys.File.Exists(absolutePath)) throw new Sys.FileNotFoundException("The referenced VPK file '" + absolutePath + "' doesn't exist.");
  181. // Leave the path as a relative path
  182. vpkFiles[i] = path;
  183. }
  184. }
  185. // Read the VPK directory. We don't process this information at the moment.
  186. Debug.Assert(header.VpkDirectoryOffset.HasValue);
  187. stream.Seek(header.VpkDirectoryOffset.Value, Sys.SeekOrigin.Begin);
  188. reader = new Sys.BinaryReader(stream);
  189. for (uint i = 0; i < header.VpkDirectoryEntryCount; ++i)
  190. {
  191. VpkDirectoryEntry entry = new VpkDirectoryEntry(reader);
  192. }
  193. }
  194. private class FileDirectoryState
  195. {
  196. public byte[] Data;
  197. public Sys.FileStream Stream;
  198. public string VpkFile;
  199. }
  200. private void ReadFileDirectoryCallback(IAsyncResult result)
  201. {
  202. FileDirectoryState state = result.AsyncState as FileDirectoryState;
  203. state.Stream.EndRead(result);
  204. state.Stream.Close();
  205. Sys.MemoryStream deflatedDirectoryStream = new Sys.MemoryStream(state.Data);
  206. InflaterInputStream deflatedDirectoryInflater = new InflaterInputStream(deflatedDirectoryStream);
  207. byte[] rawInt = new byte[4];
  208. Sys.MemoryStream intMemoryStream = new Sys.MemoryStream(rawInt, 0, rawInt.Length);
  209. Sys.BinaryReader intReader = new Sys.BinaryReader(intMemoryStream);
  210. // Uncompress file directory inflated size.
  211. try { deflatedDirectoryInflater.Read(rawInt, 0, 4); }
  212. catch (ZipException e) { throw new Sys.InvalidDataException("Error reading file directory inflated size of pak '" + state.VpkFile + "'. The compressed data is invalid.", e); }
  213. intMemoryStream.Seek(0, Sys.SeekOrigin.Begin);
  214. int inflatedDirectorySize = intReader.ReadInt32() - 4;
  215. intReader.Close();
  216. intReader = null;
  217. intMemoryStream = null;
  218. rawInt = null;
  219. Sys.BinaryReader directoryReader = new Sys.BinaryReader(deflatedDirectoryInflater, System.Text.Encoding.ASCII);
  220. // Read file directory header.
  221. uint directoryEntryCount = directoryReader.ReadUInt32();
  222. // Process file directory.
  223. for (uint i = 0; i < directoryEntryCount; ++i)
  224. {
  225. VpkFileEntry fileEntry = new VpkFileEntry(directoryReader);
  226. string filename = new string(directoryReader.ReadChars(fileEntry.NameLength));
  227. AddFile(filename, fileEntry, state.VpkFile);
  228. }
  229. state.Data = null;
  230. }
  231. private void ReadFileDirectory(Sys.Stream stream, VplHeader header)
  232. {
  233. foreach (string vpkFile in vpkFiles)
  234. {
  235. string absoluteVpkFilePath = basePath + vpkFile;
  236. Sys.FileStream vpkStream = null;
  237. try
  238. {
  239. // The System.IO.FileStream constructor may throw the following exceptions:
  240. // - FileNotFoundException: The file cannot be found.
  241. // - IOException: An I/O error occurs.
  242. // - SecurityException: The caller does not have the required permission.
  243. // - UnauthorizedAccessException: The access requested is not permitted by the operating system for the specified path.
  244. // The SecurityException exception is caught and rethrown. The others fall through.
  245. vpkStream = new Sys.FileStream(absoluteVpkFilePath, Sys.FileMode.Open, Sys.FileAccess.Read, Sys.FileShare.Read, 4, true);
  246. }
  247. catch (System.Security.SecurityException e)
  248. {
  249. throw new System.Security.SecurityException("No permission to open '" + absoluteVpkFilePath + "' for reading.", e);
  250. }
  251. Sys.BinaryReader vpkReader = new Sys.BinaryReader(vpkStream);
  252. // Seek to the end of file minus 8 bytes
  253. vpkStream.Seek(-8, Sys.SeekOrigin.End);
  254. // Read file directory offset.
  255. int directoryOffset = vpkReader.ReadInt32();
  256. // Seek to file directory.
  257. vpkStream.Seek(directoryOffset, Sys.SeekOrigin.Begin);
  258. // Read file directory deflated size.
  259. int deflatedDirectorySize = vpkReader.ReadInt32();
  260. FileDirectoryState state = new FileDirectoryState();
  261. state.Data = new byte[deflatedDirectorySize];
  262. state.Stream = vpkStream;
  263. state.VpkFile = vpkFile;
  264. vpkStream.BeginRead(state.Data, 0, deflatedDirectorySize, ReadFileDirectoryCallback, state);
  265. }
  266. }
  267. private void AddFile(string filename, VpkFileEntry entry, string vpkFile)
  268. {
  269. // We won't add the same file twice
  270. lock (files)
  271. {
  272. if (files.ContainsKey(filename)) return;
  273. }
  274. // We won't use Path.GetDirectoryName here because we want to preserve the forward slashes
  275. // for the directory name, as we will use it as a key in the dictionary.
  276. int pathEnd = Math.Max(0, filename.LastIndexOfAny(directorySeparators, filename.Length - 1, filename.Length));
  277. string directory = filename.Substring(0, pathEnd);
  278. DirectoryInfo currentDirectory = null;
  279. List<DirectoryInfo> directoriesToNotify = new List<DirectoryInfo>();
  280. // We will hold the lock on the dictionary until we finish creating all the needed directories.
  281. lock (directories)
  282. {
  283. directories.TryGetValue(directory, out currentDirectory);
  284. if (currentDirectory == null)
  285. {
  286. currentDirectory = RootDirectory;
  287. int directoryStart = 0;
  288. int directoryEnd = directory.IndexOfAny(directorySeparators, 0);
  289. if (directoryEnd == -1) directoryEnd = directory.Length;
  290. while (directoryStart < directory.Length)
  291. {
  292. string subdirectory = directory.Substring(directoryStart, directoryEnd - directoryStart);
  293. DirectoryInfo nextDirectory = currentDirectory.GetDirectory(subdirectory);
  294. if (nextDirectory == null)
  295. {
  296. // Create the directory.
  297. string newDirectoryName = directory.Substring(0, directoryEnd);
  298. nextDirectory = new DirectoryInfo(this, newDirectoryName);
  299. currentDirectory.AddChild(nextDirectory);
  300. directories.Add(newDirectoryName, nextDirectory);
  301. // We don't notify the addition of the directory here so as to prevent a deadlock
  302. // in case the invoked delegate calls a function on the filesystem that tries to
  303. // acquire the lock on 'directories'.
  304. directoriesToNotify.Add(nextDirectory);
  305. }
  306. currentDirectory = nextDirectory;
  307. directoryStart = directoryEnd + 1;
  308. if (directoryStart < directory.Length)
  309. {
  310. directoryEnd = directory.IndexOfAny(directorySeparators, directoryStart);
  311. if (directoryEnd == -1) directoryEnd = directory.Length;
  312. }
  313. }
  314. }
  315. }
  316. // Notify the addition of the directories now that we have released the lock on 'directories'.
  317. foreach (DirectoryInfo dir in directoriesToNotify) OnDirectoryAdded(dir);
  318. FileInfo file = new FileInfo(this, filename, entry.Size, vpkFile, entry.Offset);
  319. currentDirectory.AddChild(file);
  320. lock (files) files.Add(filename, file);
  321. OnFileAdded(file);
  322. }
  323. public bool FileExists(string file)
  324. {
  325. lock (files) return files.ContainsKey(file);
  326. }
  327. public bool DirectoryExists(string directory)
  328. {
  329. lock (directories) return directories.ContainsKey(directory);
  330. }
  331. public ReadOnlyCollection<string> GetDirectories(string directory)
  332. {
  333. ReadOnlyCollection<string> directoryNames = null;
  334. DirectoryInfo parentDirectory = null;
  335. lock (directories) directories.TryGetValue(directory, out parentDirectory);
  336. if (parentDirectory != null)
  337. {
  338. DirectoryInfo[] subdirectories = parentDirectory.GetDirectories();
  339. string[] names = new string[subdirectories.Length];
  340. for (int i = 0; i < subdirectories.Length; ++i) names[i] = subdirectories[i].Name;
  341. directoryNames = new ReadOnlyCollection<string>(names);
  342. }
  343. return directoryNames;
  344. }
  345. public ReadOnlyCollection<string> GetFiles(string directory)
  346. {
  347. ReadOnlyCollection<string> fileNames = null;
  348. DirectoryInfo parentDirectory = null;
  349. lock (directories) directories.TryGetValue(directory, out parentDirectory);
  350. if (parentDirectory != null)
  351. {
  352. FileInfo[] directoryFiles = parentDirectory.GetFiles();
  353. string[] names = new string[directoryFiles.Length];
  354. for (int i = 0; i < directoryFiles.Length; ++i) names[i] = directoryFiles[i].Name;
  355. fileNames = new ReadOnlyCollection<string>(names);
  356. }
  357. return fileNames;
  358. }
  359. public DirectoryInfo GetDirectoryInfo(string directory)
  360. {
  361. DirectoryInfo result = null;
  362. lock (directories) directories.TryGetValue(directory, out result);
  363. return result;
  364. }
  365. public FileInfo GetFileInfo(string file)
  366. {
  367. FileInfo result = null;
  368. lock (files) files.TryGetValue(file, out result);
  369. return result;
  370. }
  371. private void OnDirectoryAdded(DirectoryInfo directory)
  372. {
  373. if (DirectoryAdded != null) DirectoryAdded(this, new DirectoryAddedEventArgs(directory));
  374. }
  375. private void OnFileAdded(FileInfo file)
  376. {
  377. if (FileAdded != null) FileAdded(this, new FileAddedEventArgs(file));
  378. }
  379. #endregion
  380. #region Properties
  381. public DirectoryInfo RootDirectory
  382. {
  383. get { return rootDirectory; }
  384. }
  385. // Return the file count, as published on the VPL file header.
  386. /// <summary>
  387. /// Gets the number of files in this file system, as specified on the VPL file header.
  388. /// </summary>
  389. /// <remarks>Might not be the real number of files contained in the referenced VPK files.</remarks>
  390. /// <value>Number of files in this file system.</value>
  391. public int FileCount
  392. {
  393. get { return (int)header.DirectoryEntryCount; }
  394. }
  395. internal string BasePath
  396. {
  397. get { return basePath; }
  398. }
  399. #endregion
  400. #region Events
  401. public class DirectoryAddedEventArgs : EventArgs
  402. {
  403. public DirectoryAddedEventArgs(DirectoryInfo directory)
  404. {
  405. this.directory = directory;
  406. }
  407. public DirectoryInfo directory;
  408. }
  409. public class FileAddedEventArgs : EventArgs
  410. {
  411. public FileAddedEventArgs(FileInfo file)
  412. {
  413. this.file = file;
  414. }
  415. public FileInfo file;
  416. }
  417. public event EventHandler<DirectoryAddedEventArgs> DirectoryAdded;
  418. public event EventHandler<FileAddedEventArgs> FileAdded;
  419. #endregion
  420. #region Fields
  421. private VplHeader header;
  422. private string[] vpkFiles;
  423. private string basePath;
  424. private DirectoryInfo rootDirectory;
  425. private Dictionary<string, DirectoryInfo> directories = new Dictionary<string, DirectoryInfo>(StringComparer.CurrentCultureIgnoreCase);
  426. private Dictionary<string, FileInfo> files = new Dictionary<string, FileInfo>(StringComparer.CurrentCultureIgnoreCase);
  427. internal static char[] directorySeparators = new char[] { Sys.Path.DirectorySeparatorChar, Sys.Path.AltDirectorySeparatorChar };
  428. #endregion
  429. }
  430. }
  431. /* EOF */