Program.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.IO.Abstractions;
  5. using System.Linq;
  6. using System.Text;
  7. using CommandLine;
  8. using COM3D2.Toolkit.Arc;
  9. using COM3D2.Toolkit.Arc.Files;
  10. using Ganss.IO;
  11. namespace ArcToolkitCLI
  12. {
  13. interface IOptions
  14. {
  15. [Value(0, MetaName = "input", HelpText = "Input ARC files")]
  16. IEnumerable<string> Input { get; set; }
  17. }
  18. interface IOutputOptions
  19. {
  20. [Option('o', "output", HelpText = "Output directory", Default = ".")]
  21. string Output { get; set; }
  22. }
  23. interface IDecryptionOptions
  24. {
  25. [Option("arc-search-dir", Required = false,
  26. HelpText = "Directory where to search ARC file to decrypt the WARP file")]
  27. string ArcDirectory { get; set; }
  28. [Option('k', "key", HelpText = "Decryption key as a base64 string (applied to all inputs)", Required = false)]
  29. string DecryptionKey { get; set; }
  30. [Option("key-file",
  31. HelpText =
  32. "A file with decryption keys on each line. Format of the file is <decryption arc name>:<key in base64>.",
  33. Required = false)]
  34. string KeyFile { get; set; }
  35. [Option("warc", Required = false, HelpText = "WARC file to use for decryption")]
  36. string WarcFile { get; set; }
  37. }
  38. [Verb("extract", HelpText = "Extract the contents of the given ARC files")]
  39. class ExtractOptions : IOptions, IDecryptionOptions, IOutputOptions
  40. {
  41. public string ArcDirectory { get; set; }
  42. public string DecryptionKey { get; set; }
  43. public string KeyFile { get; set; }
  44. public string WarcFile { get; set; }
  45. public IEnumerable<string> Input { get; set; }
  46. public string Output { get; set; }
  47. }
  48. [Verb("info", HelpText = "Display information about the given ARC files")]
  49. class InfoOptions : IOptions
  50. {
  51. [Option("only-key",
  52. HelpText = "If the archive is a WARC file, output only the decryption key as a base64 string",
  53. Required = false)]
  54. public bool OnlyKey { get; set; }
  55. public IEnumerable<string> Input { get; set; }
  56. }
  57. [Verb("decrypt", HelpText = "Decrypts the provided WARP files")]
  58. class DecryptOptions : IOptions, IDecryptionOptions, IOutputOptions
  59. {
  60. public string ArcDirectory { get; set; }
  61. public string DecryptionKey { get; set; }
  62. public string KeyFile { get; set; }
  63. public string WarcFile { get; set; }
  64. public IEnumerable<string> Input { get; set; }
  65. public string Output { get; set; }
  66. }
  67. static class Errors
  68. {
  69. public enum ErrorCodes
  70. {
  71. NotAFile = 100,
  72. UnknownArcType,
  73. KeyNotFound,
  74. InvalidKeyFile,
  75. NoCorrectKey
  76. }
  77. public static readonly string[] ErrorMessages =
  78. {
  79. "{0} is not a file",
  80. "{0} is not a known ARC file",
  81. "No key specified or the key file does not exist",
  82. "The provided keyfile is not valid",
  83. "The ARC {0} needs a key from {1}"
  84. };
  85. public static int Error(ErrorCodes code, params object[] args)
  86. {
  87. Console.Error.WriteLine(ErrorMessages[code - ErrorCodes.NotAFile], args);
  88. return (int) code;
  89. }
  90. }
  91. static class Util
  92. {
  93. static readonly char[] GlobCharacters = {'*', '{', '}', '[', ']', '?'};
  94. public static IEnumerable<FileSystemInfoBase> EnumerateFiles(IEnumerable<string> patterns)
  95. {
  96. foreach (var pattern in patterns)
  97. if (IsGlob(pattern))
  98. foreach (var fileSystemInfoBase in Glob.Expand(pattern))
  99. yield return fileSystemInfoBase;
  100. else
  101. yield return new FileInfoWrapper(new FileSystem(), new FileInfo(pattern));
  102. }
  103. static bool IsGlob(string path) { return path.IndexOfAny(GlobCharacters) >= 0; }
  104. }
  105. class Program
  106. {
  107. static int Decrypt(DecryptOptions opts)
  108. {
  109. var errCode = GetDecryptionKeys(opts, out var keysDict);
  110. if (errCode != 0)
  111. return errCode;
  112. Directory.CreateDirectory(opts.Output);
  113. foreach (var file in Util.EnumerateFiles(opts.Input))
  114. {
  115. if (!file.Exists)
  116. return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
  117. using (var stream = File.OpenRead(file.FullName))
  118. {
  119. var header = new byte[4];
  120. stream.Read(header, 0, header.Length);
  121. var headerString = Encoding.ASCII.GetString(header);
  122. stream.Position = 0;
  123. if (headerString == "warc")
  124. Console.WriteLine($"{file.Name} is a WARC file, skipping...");
  125. else if (headerString == "warp")
  126. try
  127. {
  128. var arcStream = WarpArc.DecryptWarp(stream, requestedFile =>
  129. {
  130. if (keysDict.TryGetValue("*", out var key))
  131. return key;
  132. if (keysDict.TryGetValue(requestedFile, out key))
  133. return key;
  134. if (Directory.Exists(opts.ArcDirectory) &&
  135. File.Exists(Path.Combine(opts.ArcDirectory, requestedFile)))
  136. return ReadKeyFromFile(Path.Combine(opts.ArcDirectory, requestedFile));
  137. if (File.Exists(opts.WarcFile))
  138. return ReadKeyFromFile(opts.WarcFile);
  139. throw new FileNotFoundException("No key found for the requested ARC", requestedFile);
  140. });
  141. Console.WriteLine(
  142. $"Writing {Path.Combine(opts.Output, file.Name)} with length {arcStream.Length}");
  143. using (var output = File.Create(Path.Combine(opts.Output, file.Name)))
  144. arcStream.CopyTo(output);
  145. }
  146. catch (FileNotFoundException fe)
  147. {
  148. return Errors.Error(Errors.ErrorCodes.NoCorrectKey, file.Name, fe.FileName);
  149. }
  150. else
  151. return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
  152. }
  153. }
  154. return 0;
  155. }
  156. static int DisplayInfo(InfoOptions opts)
  157. {
  158. var first = true;
  159. foreach (var file in Util.EnumerateFiles(opts.Input))
  160. {
  161. if (!file.Exists)
  162. return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
  163. if (!first && !opts.OnlyKey)
  164. Console.WriteLine();
  165. first = false;
  166. using (var stream = File.OpenRead(file.FullName))
  167. {
  168. var header = new byte[4];
  169. stream.Read(header, 0, header.Length);
  170. var headerString = Encoding.ASCII.GetString(header);
  171. stream.Position = 0;
  172. if (headerString == "warc")
  173. {
  174. var key = new byte[2048];
  175. stream.Read(key, 0, key.Length);
  176. var keyBase64 = Convert.ToBase64String(key);
  177. if (opts.OnlyKey)
  178. {
  179. Console.WriteLine($"{Path.GetFileName(file.FullName)}:{keyBase64}");
  180. continue;
  181. }
  182. stream.Position = 0;
  183. using (var warc = new WarcArc(stream))
  184. {
  185. Console.WriteLine($"File name: {file.Name}");
  186. Console.WriteLine("ARC type: WARC");
  187. Console.WriteLine($"ARC Name: {warc.Name}");
  188. Console.WriteLine($"File count: {warc.Entries.Count()}");
  189. Console.WriteLine($"Decryption key: {keyBase64}");
  190. }
  191. }
  192. else if (headerString == "warp")
  193. {
  194. if (opts.OnlyKey)
  195. continue;
  196. Console.WriteLine($"File name: {file.Name}");
  197. Console.WriteLine("ARC type: WARP");
  198. using (var br = new BinaryReader(stream))
  199. Console.WriteLine(
  200. $"Needs a decryption key from the following ARC: {WarpArc.GetKeyWarpName(br)}");
  201. }
  202. else
  203. return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
  204. }
  205. }
  206. return 0;
  207. }
  208. static int Extract(ExtractOptions opts)
  209. {
  210. Dictionary<string, byte[]> keys = null;
  211. Directory.CreateDirectory(opts.Output);
  212. foreach (var file in Util.EnumerateFiles(opts.Input))
  213. {
  214. if (!file.Exists)
  215. return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
  216. using (var stream = File.OpenRead(file.FullName))
  217. {
  218. var header = new byte[4];
  219. stream.Read(header, 0, header.Length);
  220. var headerString = Encoding.ASCII.GetString(header);
  221. stream.Position = 0;
  222. WarcArc arc = null;
  223. if (headerString == "warp")
  224. {
  225. if (keys == null)
  226. {
  227. var err = GetDecryptionKeys(opts, out keys);
  228. if (err != 0)
  229. return err;
  230. arc = new WarpArc(stream, requestedFile =>
  231. {
  232. if (keys.TryGetValue("*", out var key))
  233. return key;
  234. if (keys.TryGetValue(requestedFile, out key))
  235. return key;
  236. if (Directory.Exists(opts.ArcDirectory) &&
  237. File.Exists(Path.Combine(opts.ArcDirectory, requestedFile)))
  238. return ReadKeyFromFile(Path.Combine(opts.ArcDirectory, requestedFile));
  239. if (File.Exists(opts.WarcFile))
  240. return ReadKeyFromFile(opts.WarcFile);
  241. throw new FileNotFoundException("No key found for the requested ARC", requestedFile);
  242. });
  243. }
  244. }
  245. else if (headerString == "warc")
  246. arc = new WarcArc(stream);
  247. if (arc == null)
  248. return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
  249. Console.WriteLine($"Extracting {arc.Name}:");
  250. var arcDir = Path.Combine(opts.Output, Path.GetFileNameWithoutExtension(arc.Name));
  251. Directory.CreateDirectory(arcDir);
  252. foreach (var arcEntry in arc.Entries)
  253. ExtractArchive(arcEntry, arcDir);
  254. arc.Dispose();
  255. }
  256. }
  257. return 0;
  258. }
  259. static void ExtractArchive(ArcEntry entry, string path)
  260. {
  261. switch (entry)
  262. {
  263. case ArcFileEntry fe:
  264. var fullName = Path.Combine(path, fe.Name);
  265. Console.WriteLine(fullName);
  266. using (var file = File.Create(fullName))
  267. using (var data = fe.GetDataStream())
  268. data.CopyTo(file);
  269. break;
  270. case ArcDirectoryEntry de:
  271. var subfolderPath = Path.Combine(path, de.Name);
  272. Directory.CreateDirectory(subfolderPath);
  273. foreach (var arcEntry in de.Children)
  274. ExtractArchive(arcEntry, subfolderPath);
  275. break;
  276. }
  277. }
  278. static int GetDecryptionKeys(IDecryptionOptions opts, out Dictionary<string, byte[]> keys)
  279. {
  280. keys = new Dictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
  281. if (!string.IsNullOrWhiteSpace(opts.KeyFile))
  282. {
  283. if (!File.Exists(opts.KeyFile))
  284. return Errors.Error(Errors.ErrorCodes.KeyNotFound);
  285. foreach (var line in File.ReadAllLines(opts.KeyFile))
  286. {
  287. var parts = line.Split(':');
  288. if (parts.Length != 2)
  289. return Errors.Error(Errors.ErrorCodes.InvalidKeyFile);
  290. keys[parts[0].Trim()] = Convert.FromBase64String(parts[1].Trim());
  291. }
  292. }
  293. else if (!string.IsNullOrWhiteSpace(opts.DecryptionKey))
  294. keys["*"] = Convert.FromBase64String(opts.DecryptionKey);
  295. else if (string.IsNullOrWhiteSpace(opts.ArcDirectory) && string.IsNullOrWhiteSpace(opts.WarcFile))
  296. return Errors.Error(Errors.ErrorCodes.KeyNotFound);
  297. return 0;
  298. }
  299. static int Main(string[] args)
  300. {
  301. return Parser.Default.ParseArguments<InfoOptions, ExtractOptions, DecryptOptions>(args).MapResult(
  302. (InfoOptions opts) => DisplayInfo(opts), (ExtractOptions opts) => Extract(opts),
  303. (DecryptOptions opts) => Decrypt(opts), errs => 1);
  304. }
  305. static byte[] ReadKeyFromFile(string filename)
  306. {
  307. using (var br = new BinaryReader(File.OpenRead(filename)))
  308. return br.ReadBytes(2048);
  309. }
  310. }
  311. }