NeiConverter.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using ArcToolkitCLI.Commands.Options;
  7. using ArcToolkitCLI.Util;
  8. using CommandLine;
  9. namespace ArcToolkitCLI.Commands.Converters
  10. {
  11. [Verb("nei", HelpText = "Convert NEI files into CSV and back")]
  12. internal class NeiConverter : IConverterCommand, IInputOptions, IOutputOptions
  13. {
  14. private static readonly byte[] NEI_KEY =
  15. {
  16. 0xAA,
  17. 0xC9,
  18. 0xD2,
  19. 0x35,
  20. 0x22,
  21. 0x87,
  22. 0x20,
  23. 0xF2,
  24. 0x40,
  25. 0xC5,
  26. 0x61,
  27. 0x7C,
  28. 0x01,
  29. 0xDF,
  30. 0x66,
  31. 0x54
  32. };
  33. private static readonly byte[] NEI_MAGIC =
  34. {
  35. 0x77,
  36. 0x73,
  37. 0x76,
  38. 0xFF
  39. };
  40. private static readonly Encoding ShiftJisEncoding = Encoding.GetEncoding(932);
  41. [Option('s', "separator", Default = ';', HelpText = "Value separator of the CSV file")]
  42. public char ValueSeparator { get; set; }
  43. public int Run()
  44. {
  45. var files = Glob.EnumerateFiles(Input);
  46. if (!files.Any())
  47. {
  48. Console.WriteLine("No files specified. Run `convert help nei` for help.");
  49. return 0;
  50. }
  51. foreach (var file in files)
  52. {
  53. var result = 0;
  54. switch (file.Extension.ToLowerInvariant())
  55. {
  56. case ".nei":
  57. if ((result = ToCSV(file.FullName)) != 0)
  58. return result;
  59. break;
  60. case ".csv":
  61. if ((result = ToNei(file.FullName)) != 0)
  62. return result;
  63. break;
  64. default:
  65. Console.WriteLine($"File {file.FullName} is neither .nei nor .csv file. Skipping...");
  66. break;
  67. }
  68. }
  69. return 0;
  70. }
  71. [Value(0, HelpText = "Input NEI or CSV files")]
  72. public IEnumerable<string> Input { get; set; }
  73. public string Output { get; set; }
  74. private List<List<string>> ParseCSV(TextReader tr)
  75. {
  76. var buffer = new StringBuilder();
  77. var whitespaceBuffer = new StringBuilder();
  78. var isQuoted = false;
  79. var quoteLevel = 0;
  80. var result = new List<List<string>>();
  81. var line = new List<string>();
  82. int nextChar;
  83. while ((nextChar = tr.Read()) != -1)
  84. {
  85. var c = (char)nextChar;
  86. if (c == '\n' && !isQuoted)
  87. {
  88. if (buffer.Length != 0)
  89. {
  90. line.Add(buffer.ToString());
  91. buffer.Clear();
  92. }
  93. if (line.Count != 0)
  94. result.Add(line);
  95. line = new List<string>();
  96. whitespaceBuffer.Clear();
  97. continue;
  98. }
  99. var isWhitespace = char.IsWhiteSpace(c);
  100. var shouldSeparate = c == ValueSeparator && (!isQuoted || quoteLevel % 2 == 0);
  101. if (isWhitespace)
  102. {
  103. whitespaceBuffer.Append(c);
  104. continue;
  105. }
  106. if (whitespaceBuffer.Length != 0)
  107. {
  108. if (buffer.Length > 0 && !shouldSeparate)
  109. buffer.Append(whitespaceBuffer);
  110. whitespaceBuffer.Clear();
  111. }
  112. if (shouldSeparate)
  113. {
  114. line.Add(buffer.ToString());
  115. buffer.Clear();
  116. quoteLevel = 0;
  117. isQuoted = false;
  118. }
  119. else if (c == '"')
  120. {
  121. if (buffer.Length == 0 && quoteLevel == 0)
  122. {
  123. isQuoted = true;
  124. quoteLevel++;
  125. whitespaceBuffer.Clear();
  126. continue;
  127. }
  128. if (isQuoted)
  129. quoteLevel++;
  130. if (!isQuoted || quoteLevel % 2 == 1)
  131. buffer.Append(c);
  132. }
  133. else
  134. {
  135. if (isQuoted && quoteLevel != 0 && quoteLevel % 2 == 0)
  136. {
  137. line.Clear();
  138. tr.ReadLine();
  139. continue;
  140. }
  141. buffer.Append(c);
  142. }
  143. }
  144. if (buffer.Length != 0)
  145. line.Add(buffer.ToString());
  146. if (line.Count != 0)
  147. result.Add(line);
  148. return result;
  149. }
  150. private int ToNei(string filePath)
  151. {
  152. var nameNoExt = Path.GetFileNameWithoutExtension(filePath);
  153. List<List<string>> values;
  154. using (var tr = File.OpenText(filePath))
  155. values = ParseCSV(tr);
  156. var cols = values.Max(l => l.Count);
  157. var rows = values.Count;
  158. var encodedValues = new byte[rows * cols][];
  159. for (var rowIndex = 0; rowIndex < values.Count; rowIndex++)
  160. {
  161. var row = values[rowIndex];
  162. for (var colIndex = 0; colIndex < cols; colIndex++)
  163. encodedValues[colIndex + rowIndex * cols] = colIndex < row.Count
  164. ? ShiftJisEncoding.GetBytes(row[colIndex])
  165. : new byte[0];
  166. }
  167. using var ms = new MemoryStream();
  168. using var bw = new BinaryWriter(ms);
  169. bw.Write(NEI_MAGIC);
  170. bw.Write(cols);
  171. bw.Write(rows);
  172. var totalLength = 0;
  173. foreach (var encodedValue in encodedValues)
  174. {
  175. var len = encodedValue.Length;
  176. if (len != 0)
  177. len++;
  178. bw.Write(encodedValue.Length == 0 ? 0 : totalLength);
  179. bw.Write(len);
  180. totalLength += len;
  181. }
  182. for (var i = 0; i < encodedValues.Length; i++)
  183. {
  184. var encodedValue = encodedValues[i];
  185. if (encodedValue.Length == 0)
  186. continue;
  187. bw.Write(encodedValue);
  188. if (i != encodedValue.Length - 1)
  189. bw.Write((byte)0x00);
  190. }
  191. var data = ms.ToArray();
  192. File.WriteAllBytes(Path.Combine(Output, $"{nameNoExt}.nei"), Encryption.EncryptBytes(data, NEI_KEY));
  193. return 0;
  194. }
  195. private string Escape(string value)
  196. {
  197. if (!value.Contains(ValueSeparator) && !value.Contains('\n'))
  198. return value;
  199. return $"\"{value.Replace("\"", "\"\"")}\"";
  200. }
  201. private int ToCSV(string filePath)
  202. {
  203. var nameNoExt = Path.GetFileNameWithoutExtension(filePath);
  204. var neiData = Encryption.DecryptBytes(File.ReadAllBytes(filePath), NEI_KEY);
  205. using var ms = new MemoryStream(neiData);
  206. using var br = new BinaryReader(ms);
  207. if (!br.ReadBytes(4).SequenceEqual(NEI_MAGIC))
  208. {
  209. Console.WriteLine($"File {filePath} is not a valid NEI file");
  210. return 1;
  211. }
  212. var cols = br.ReadUInt32();
  213. var rows = br.ReadUInt32();
  214. var strLengths = new int[cols * rows];
  215. for (var cell = 0; cell < cols * rows; cell++)
  216. {
  217. br.ReadInt32(); // Total length of all strings because why not
  218. strLengths[cell] = br.ReadInt32();
  219. }
  220. var values = new string[cols * rows];
  221. for (var cell = 0; cell < cols * rows; cell++)
  222. {
  223. var len = strLengths[cell];
  224. values[cell] = ShiftJisEncoding.GetString(br.ReadBytes(len), 0, Math.Max(len - 1, 0));
  225. }
  226. using var tw = File.CreateText(Path.Combine(Output, $"{nameNoExt}.csv"));
  227. for (var row = 0; row < rows; row++)
  228. {
  229. for (var col = 0; col < cols; col++)
  230. {
  231. tw.Write(Escape(values[row * cols + col]));
  232. if (col != cols - 1)
  233. tw.Write(ValueSeparator);
  234. }
  235. tw.WriteLine();
  236. }
  237. return 0;
  238. }
  239. }
  240. }