spry-mkssr.vala 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. namespace Spry.Tools {
  5. public class Mkssr : Object {
  6. private static bool show_version = false;
  7. private static string? output_file = null;
  8. private static string? content_type_override = null;
  9. private static string? resource_name_override = null;
  10. private static bool generate_vala = false;
  11. private static string? namespace_name = null;
  12. private const OptionEntry[] options = {
  13. { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output file name (default: input.ssr or ClassNameResource.vala)", "FILE" },
  14. { "content-type", 'c', 0, OptionArg.STRING, ref content_type_override, "Override content type (e.g., text/html)", "TYPE" },
  15. { "name", 'n', 0, OptionArg.STRING, ref resource_name_override, "Override resource name (default: input filename)", "NAME" },
  16. { "vala", '\0', 0, OptionArg.NONE, ref generate_vala, "Generate Vala source file instead of SSR", null },
  17. { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Namespace for generated Vala class (requires --vala)", "NAMESPACE" },
  18. { "version", 'v', 0, OptionArg.NONE, ref show_version, "Show version information", null },
  19. { null }
  20. };
  21. public static int main(string[] args) {
  22. try {
  23. var opt_context = new OptionContext("INPUT_FILE - Generate a Spry Static Resource file");
  24. opt_context.set_help_enabled(true);
  25. opt_context.add_main_entries(options, null);
  26. opt_context.parse(ref args);
  27. } catch (OptionError e) {
  28. stderr.printf("Error: %s\n", e.message);
  29. stderr.printf("Run '%s --help' for more information.\n", args[0]);
  30. return 1;
  31. }
  32. if (show_version) {
  33. stdout.printf("spry-mkssr 0.1\n");
  34. return 0;
  35. }
  36. // Validate --ns requires --vala
  37. if (namespace_name != null && !generate_vala) {
  38. stderr.printf("Error: --ns requires --vala flag.\n");
  39. return 1;
  40. }
  41. // Get input file from remaining arguments
  42. string? input_file = null;
  43. if (args.length > 1) {
  44. input_file = args[1];
  45. }
  46. if (input_file == null) {
  47. stderr.printf("Error: No input file specified.\n");
  48. stderr.printf("Run '%s --help' for more information.\n", args[0]);
  49. return 1;
  50. }
  51. // Determine output file and class name base
  52. string actual_output;
  53. string? class_name_base = null;
  54. if (output_file != null) {
  55. actual_output = output_file;
  56. } else if (generate_vala) {
  57. // Generate Vala filename from resource name (if -n specified) or input file
  58. class_name_base = resource_name_override ?? Path.get_basename(input_file);
  59. actual_output = make_pascal_case(class_name_base) + "Resource.vala";
  60. } else {
  61. actual_output = input_file + ".ssr";
  62. }
  63. try {
  64. var tool = new Mkssr();
  65. if (generate_vala) {
  66. tool.generate_vala_source(input_file, actual_output, content_type_override, resource_name_override, namespace_name, class_name_base);
  67. } else {
  68. tool.generate_ssr(input_file, actual_output, content_type_override, resource_name_override);
  69. }
  70. stdout.printf("Generated: %s\n", actual_output);
  71. return 0;
  72. } catch (Error e) {
  73. stderr.printf("Error: %s\n", e.message);
  74. return 1;
  75. }
  76. }
  77. public void generate_ssr(string input_path, string output_path, string? content_type_override, string? resource_name_override) throws Error {
  78. // Read input file
  79. var input_file = File.new_for_path(input_path);
  80. if (!input_file.query_exists()) {
  81. throw new IOError.NOT_FOUND(@"Input file '$input_path' does not exist");
  82. }
  83. var file_info = input_file.query_info("standard::size", 0);
  84. var file_size = (size_t)file_info.get_size();
  85. var input_stream = new DataInputStream(input_file.read());
  86. var input_bytes = input_stream.read_bytes(file_size);
  87. input_stream.close();
  88. // Get the data as uint8[] (copy to ensure we own it)
  89. var input_data = new uint8[input_bytes.get_size()];
  90. Memory.copy(input_data, input_bytes.get_data(), input_bytes.get_size());
  91. // Get file name and content type
  92. var name = resource_name_override ?? Path.get_basename(input_path);
  93. string content_type;
  94. if (content_type_override != null) {
  95. content_type = content_type_override;
  96. } else {
  97. content_type = guess_content_type(name, input_data);
  98. }
  99. // Compute hash for ETag (SHA-512 = 64 bytes)
  100. var hash = compute_hash(input_data);
  101. // Compress with all encodings at highest compression using Astralis compressors
  102. var encodings = new List<EncodedData>();
  103. // Identity (no compression) - always included
  104. var identity_data = new uint8[input_data.length];
  105. Memory.copy(identity_data, input_data, input_data.length);
  106. encodings.append(new EncodedData("identity", (owned)identity_data));
  107. // Create ByteBuffer from input data for compression
  108. var input_buffer = new ByteBuffer.from_byte_array((owned)input_data);
  109. // GZip at highest compression
  110. stdout.printf("Compressing with gzip...\n");
  111. var gzip_compressor = new GzipCompressor(9);
  112. var gzip_compressed = gzip_compressor.compress_buffer(input_buffer, null);
  113. var gzip_data = gzip_compressed.to_array();
  114. stdout.printf(" gzip: %zu -> %zu bytes (%.1f%%)\n",
  115. file_size, gzip_compressed.length,
  116. 100.0 * gzip_compressed.length / file_size);
  117. // Only add if smaller than original
  118. if (gzip_compressed.length < file_size) {
  119. encodings.append(new EncodedData("gzip", (owned)gzip_data));
  120. } else {
  121. stdout.printf(" Skipping gzip (not smaller than original)\n");
  122. }
  123. // Zstandard at highest compression
  124. stdout.printf("Compressing with zstd...\n");
  125. var zstd_compressor = new ZstdCompressor(19);
  126. var zstd_compressed = zstd_compressor.compress_buffer(input_buffer, null);
  127. var zstd_data = zstd_compressed.to_array();
  128. stdout.printf(" zstd: %zu -> %zu bytes (%.1f%%)\n",
  129. file_size, zstd_compressed.length,
  130. 100.0 * zstd_compressed.length / file_size);
  131. // Only add if smaller than original
  132. if (zstd_compressed.length < file_size) {
  133. encodings.append(new EncodedData("zstd", (owned)zstd_data));
  134. } else {
  135. stdout.printf(" Skipping zstd (not smaller than original)\n");
  136. }
  137. // Brotli at highest compression
  138. stdout.printf("Compressing with brotli...\n");
  139. var brotli_compressor = new BrotliCompressor(11);
  140. var brotli_compressed = brotli_compressor.compress_buffer(input_buffer, null);
  141. var brotli_data = brotli_compressed.to_array();
  142. stdout.printf(" brotli: %zu -> %zu bytes (%.1f%%)\n",
  143. file_size, brotli_compressed.length,
  144. 100.0 * brotli_compressed.length / file_size);
  145. // Only add if smaller than original
  146. if (brotli_compressed.length < file_size) {
  147. encodings.append(new EncodedData("br", (owned)brotli_data));
  148. } else {
  149. stdout.printf(" Skipping brotli (not smaller than original)\n");
  150. }
  151. // Write output file
  152. write_ssr_file(output_path, name, content_type, hash, encodings);
  153. }
  154. public void generate_vala_source(string input_path, string output_path, string? content_type_override, string? resource_name_override, string? namespace_name, string? class_name_base) throws Error {
  155. // Read input file
  156. var input_file = File.new_for_path(input_path);
  157. if (!input_file.query_exists()) {
  158. throw new IOError.NOT_FOUND(@"Input file '$input_path' does not exist");
  159. }
  160. var file_info = input_file.query_info("standard::size", 0);
  161. var file_size = (size_t)file_info.get_size();
  162. var input_stream = new DataInputStream(input_file.read());
  163. var input_bytes = input_stream.read_bytes(file_size);
  164. input_stream.close();
  165. // Get the data as uint8[] (copy to ensure we own it)
  166. var input_data = new uint8[input_bytes.get_size()];
  167. Memory.copy(input_data, input_bytes.get_data(), input_bytes.get_size());
  168. // Get file name and content type
  169. var name = resource_name_override ?? Path.get_basename(input_path);
  170. string content_type;
  171. if (content_type_override != null) {
  172. content_type = content_type_override;
  173. } else {
  174. content_type = guess_content_type(name, input_data);
  175. }
  176. // Compute hash for ETag (SHA-512 hex string)
  177. var hash_hex = compute_hash_hex(input_data);
  178. // Determine class name: use class_name_base if provided (from -n flag), otherwise from output path
  179. string class_name;
  180. if (class_name_base != null) {
  181. class_name = make_pascal_case(class_name_base) + "Resource";
  182. } else {
  183. class_name = Path.get_basename(output_path);
  184. if (class_name.has_suffix(".vala")) {
  185. class_name = class_name.substring(0, class_name.length - 5);
  186. }
  187. }
  188. // Compress with all encodings at highest compression using Astralis compressors
  189. var encodings = new List<EncodedData>();
  190. // Identity (no compression) - always included
  191. var identity_data = new uint8[input_data.length];
  192. Memory.copy(identity_data, input_data, input_data.length);
  193. encodings.append(new EncodedData("identity", (owned)identity_data));
  194. // Create ByteBuffer from input data for compression
  195. var input_buffer = new ByteBuffer.from_byte_array((owned)input_data);
  196. // GZip at highest compression
  197. stdout.printf("Compressing with gzip...\n");
  198. var gzip_compressor = new GzipCompressor(9);
  199. var gzip_compressed = gzip_compressor.compress_buffer(input_buffer, null);
  200. var gzip_data = gzip_compressed.to_array();
  201. stdout.printf(" gzip: %zu -> %zu bytes (%.1f%%)\n",
  202. file_size, gzip_compressed.length,
  203. 100.0 * gzip_compressed.length / file_size);
  204. // Only add if smaller than original
  205. if (gzip_compressed.length < file_size) {
  206. encodings.append(new EncodedData("gzip", (owned)gzip_data));
  207. } else {
  208. stdout.printf(" Skipping gzip (not smaller than original)\n");
  209. }
  210. // Zstandard at highest compression
  211. stdout.printf("Compressing with zstd...\n");
  212. var zstd_compressor = new ZstdCompressor(19);
  213. var zstd_compressed = zstd_compressor.compress_buffer(input_buffer, null);
  214. var zstd_data = zstd_compressed.to_array();
  215. stdout.printf(" zstd: %zu -> %zu bytes (%.1f%%)\n",
  216. file_size, zstd_compressed.length,
  217. 100.0 * zstd_compressed.length / file_size);
  218. // Only add if smaller than original
  219. if (zstd_compressed.length < file_size) {
  220. encodings.append(new EncodedData("zstd", (owned)zstd_data));
  221. } else {
  222. stdout.printf(" Skipping zstd (not smaller than original)\n");
  223. }
  224. // Brotli at highest compression
  225. stdout.printf("Compressing with brotli...\n");
  226. var brotli_compressor = new BrotliCompressor(11);
  227. var brotli_compressed = brotli_compressor.compress_buffer(input_buffer, null);
  228. var brotli_data = brotli_compressed.to_array();
  229. stdout.printf(" brotli: %zu -> %zu bytes (%.1f%%)\n",
  230. file_size, brotli_compressed.length,
  231. 100.0 * brotli_compressed.length / file_size);
  232. // Only add if smaller than original
  233. if (brotli_compressed.length < file_size) {
  234. encodings.append(new EncodedData("br", (owned)brotli_data));
  235. } else {
  236. stdout.printf(" Skipping brotli (not smaller than original)\n");
  237. }
  238. // Write Vala source file
  239. write_vala_file(output_path, class_name, name, content_type, hash_hex, encodings, namespace_name);
  240. }
  241. private string guess_content_type(string filename, uint8[] sample_data) {
  242. bool result_uncertain;
  243. var content_type = ContentType.guess(filename, sample_data, out result_uncertain);
  244. var mime = ContentType.get_mime_type(content_type);
  245. return mime ?? "application/octet-stream";
  246. }
  247. private uint8[] compute_hash(uint8[] data) {
  248. // Use SHA-512 for the hash (64 bytes)
  249. var checksum = new Checksum(ChecksumType.SHA512);
  250. checksum.update(data, data.length);
  251. // Get raw bytes of the hash
  252. var hex_string = checksum.get_string();
  253. var hash_bytes = new uint8[64];
  254. for (var i = 0; i < 64; i++) {
  255. var hex_byte = hex_string.substring(i * 2, 2);
  256. // Parse hex byte manually
  257. int val = 0;
  258. for (int j = 0; j < 2; j++) {
  259. val *= 16;
  260. var c = hex_byte.get(j);
  261. if (c >= '0' && c <= '9') {
  262. val += c - '0';
  263. } else if (c >= 'a' && c <= 'f') {
  264. val += c - 'a' + 10;
  265. } else if (c >= 'A' && c <= 'F') {
  266. val += c - 'A' + 10;
  267. }
  268. }
  269. hash_bytes[i] = (uint8)val;
  270. }
  271. return hash_bytes;
  272. }
  273. private string compute_hash_hex(uint8[] data) {
  274. var checksum = new Checksum(ChecksumType.SHA512);
  275. checksum.update(data, data.length);
  276. return checksum.get_string();
  277. }
  278. private void write_ssr_file(string path, string name, string content_type, uint8[] hash, List<EncodedData> encodings) throws Error {
  279. var output_file = File.new_for_path(path);
  280. var output_stream = new DataOutputStream(output_file.replace(null, false, FileCreateFlags.NONE));
  281. // Write magic number: "spry-sr\0"
  282. output_stream.put_byte('s');
  283. output_stream.put_byte('p');
  284. output_stream.put_byte('r');
  285. output_stream.put_byte('y');
  286. output_stream.put_byte('-');
  287. output_stream.put_byte('s');
  288. output_stream.put_byte('r');
  289. output_stream.put_byte(0);
  290. // Write name field
  291. write_string_field(output_stream, name);
  292. // Write content type field
  293. write_string_field(output_stream, content_type);
  294. // Write hash (64 bytes)
  295. output_stream.write(hash);
  296. // Calculate header size to determine starting offset
  297. var header_size = 8; // magic
  298. header_size += 1 + name.data.length; // name field
  299. header_size += 1 + content_type.data.length; // content type field
  300. header_size += 64; // hash
  301. header_size += 1; // encoding count byte
  302. // Add size of each encoding header
  303. foreach (var encoding in encodings) {
  304. header_size += 1 + encoding.type.data.length; // type string field
  305. header_size += 8; // offset
  306. header_size += 8; // size
  307. }
  308. // Write encoding count
  309. output_stream.put_byte((uint8)encodings.length());
  310. // Write encoding headers with offsets
  311. uint64 current_offset = header_size;
  312. foreach (var encoding in encodings) {
  313. write_string_field(output_stream, encoding.type);
  314. output_stream.put_uint64(current_offset);
  315. output_stream.put_uint64(encoding.data.length);
  316. current_offset += encoding.data.length;
  317. }
  318. // Write encoding data
  319. foreach (var encoding in encodings) {
  320. output_stream.write(encoding.data);
  321. }
  322. output_stream.close();
  323. }
  324. private void write_vala_file(string path, string class_name, string name, string content_type, string hash_hex, List<EncodedData> encodings, string? namespace_name) throws Error {
  325. var output_file = File.new_for_path(path);
  326. var output_stream = new DataOutputStream(output_file.replace(null, false, FileCreateFlags.NONE));
  327. // Determine indentation based on namespace
  328. string indent = namespace_name != null ? " " : "";
  329. string indent2 = namespace_name != null ? " " : " ";
  330. string indent3 = namespace_name != null ? " " : " ";
  331. string indent4 = namespace_name != null ? " " : " ";
  332. // Write using statements
  333. output_stream.put_string("using Spry;\n");
  334. output_stream.put_string("using Invercargill;\n\n");
  335. // Write namespace if provided
  336. if (namespace_name != null) {
  337. output_stream.put_string(@"namespace $(namespace_name) {\n\n");
  338. }
  339. // Write comment and class declaration
  340. output_stream.put_string(@"$(indent)// Generated by spry-mkssr\n");
  341. output_stream.put_string(@"$(indent)public class $(class_name) : ConstantStaticResource {\n\n");
  342. // Write properties
  343. output_stream.put_string(@"$(indent2)public override string name { get { return \"$name\"; } }\n");
  344. output_stream.put_string(@"$(indent2)public override string file_hash { get { return \"$hash_hex\"; } }\n");
  345. output_stream.put_string(@"$(indent2)public override string content_type { get { return \"$content_type\"; } }\n\n");
  346. // Write get_best_encoding method
  347. output_stream.put_string(@"$(indent2)public override string get_best_encoding(Set<string> supported) {\n");
  348. // Sort encodings by size (smallest first) - build a sorted array
  349. var sorted_encodings = new EncodedData[encodings.length()];
  350. int idx = 0;
  351. foreach (var enc in encodings) {
  352. sorted_encodings[idx++] = enc;
  353. }
  354. // Simple bubble sort by size
  355. for (int i = 0; i < sorted_encodings.length - 1; i++) {
  356. for (int j = i + 1; j < sorted_encodings.length; j++) {
  357. if (sorted_encodings[i].data.length > sorted_encodings[j].data.length) {
  358. var tmp = sorted_encodings[i];
  359. sorted_encodings[i] = sorted_encodings[j];
  360. sorted_encodings[j] = tmp;
  361. }
  362. }
  363. }
  364. // Check in order of smallest to largest
  365. foreach (var encoding in sorted_encodings) {
  366. if (encoding.type == "identity") continue; // Handle identity last
  367. output_stream.put_string(@"$(indent3)if (supported.has(\"$(encoding.type)\")) return \"$(encoding.type)\";\n");
  368. }
  369. output_stream.put_string(@"$(indent3)return \"identity\";\n");
  370. output_stream.put_string(@"$(indent2)}\n\n");
  371. // Write get_encoding method
  372. output_stream.put_string(@"$(indent2)public override unowned uint8[] get_encoding(string encoding) {\n");
  373. output_stream.put_string(@"$(indent3)switch (encoding) {\n");
  374. foreach (var encoding in encodings) {
  375. var var_name = encoding_name_to_const(encoding.type);
  376. output_stream.put_string(@"$(indent4)case \"$(encoding.type)\": return $var_name;\n");
  377. }
  378. output_stream.put_string(@"$(indent4)default: return IDENTITY_DATA;\n");
  379. output_stream.put_string(@"$(indent3)}\n");
  380. output_stream.put_string(@"$(indent2)}\n\n");
  381. // Write const arrays for each encoding
  382. foreach (var encoding in encodings) {
  383. var var_name = encoding_name_to_const(encoding.type);
  384. output_stream.put_string(@"$(indent2)private const uint8[] $var_name = {\n");
  385. write_byte_array(output_stream, encoding.data, indent3);
  386. output_stream.put_string(@"\n$(indent2)};\n\n");
  387. }
  388. // Close class
  389. output_stream.put_string(@"$(indent)}\n");
  390. // Close namespace if provided
  391. if (namespace_name != null) {
  392. output_stream.put_string("}\n");
  393. }
  394. output_stream.close();
  395. }
  396. private string encoding_name_to_const(string encoding_type) {
  397. // Convert encoding type to ALL_CAPS const name
  398. // e.g., "identity" -> "IDENTITY_DATA", "gzip" -> "GZIP_DATA", "br" -> "BR_DATA"
  399. var upper = encoding_type.replace("-", "_").up();
  400. return @"$(upper)_DATA";
  401. }
  402. private void write_byte_array(DataOutputStream stream, uint8[] data, string indent) throws Error {
  403. for (int i = 0; i < data.length; i++) {
  404. if (i > 0) {
  405. if (i % 16 == 0) {
  406. stream.put_string(",\n" + indent);
  407. } else {
  408. stream.put_string(", ");
  409. }
  410. } else {
  411. stream.put_string(indent);
  412. }
  413. stream.put_string("0x%02x".printf(data[i]));
  414. }
  415. }
  416. private void write_string_field(DataOutputStream stream, string value) throws Error {
  417. var bytes = value.data;
  418. if (bytes.length > 255) {
  419. throw new IOError.INVALID_DATA(@"String field too long: $(value.length) bytes (max 255)");
  420. }
  421. stream.put_byte((uint8)bytes.length);
  422. stream.write(bytes);
  423. }
  424. private static string make_pascal_case(string input) {
  425. // Remove extension first
  426. var name = input;
  427. var dot_pos = name.last_index_of(".");
  428. if (dot_pos > 0) {
  429. name = name.substring(0, dot_pos);
  430. }
  431. // Split on common separators and capitalize each word
  432. var result = new StringBuilder();
  433. var capitalize_next = true;
  434. foreach (var c in name.to_utf8()) {
  435. if (c == '-' || c == '_' || c == ' ' || c == '.') {
  436. capitalize_next = true;
  437. } else {
  438. if (capitalize_next) {
  439. result.append(c.toupper().to_string());
  440. capitalize_next = false;
  441. } else {
  442. result.append(c.to_string());
  443. }
  444. }
  445. }
  446. return result.str;
  447. }
  448. private class EncodedData {
  449. public string type;
  450. public uint8[] data;
  451. public EncodedData(string type, owned uint8[] data) {
  452. this.type = type;
  453. this.data = (owned)data;
  454. }
  455. }
  456. }
  457. }