statum-mkres.vala 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. namespace Statum.Tools {
  5. /**
  6. * `statum-mkres` — generates a {@link Statum.ConstantStatumResource} Vala
  7. * subclass from an input file, precompressing it with identity/gzip/zstd/br.
  8. *
  9. * Mirrors `spry-mkssr --vala`. Intended to be wired as a meson
  10. * `custom_target` (see `tools/meson.build`).
  11. *
  12. * statum-mkres -o LogoResource.vala -n logo.png logo.png
  13. * statum-mkres --ns MyApp.Static -c text/css -o Styles.vala styles.css
  14. */
  15. public class Mkres : Object {
  16. private static string? output_file = null;
  17. private static string? content_type_override = null;
  18. private static string? resource_name_override = null;
  19. private static string? class_name_override = null;
  20. private static string? namespace_name = null;
  21. private const OptionEntry[] options = {
  22. { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output Vala file name (default: <InputName>Resource.vala)", "FILE" },
  23. { "content-type", 'c', 0, OptionArg.STRING, ref content_type_override, "Override content type (e.g. text/html)", "TYPE" },
  24. { "name", 'n', 0, OptionArg.STRING, ref resource_name_override, "Resource name (default: input filename)", "NAME" },
  25. { "class-name", '\0', 0, OptionArg.STRING, ref class_name_override, "Generated Vala class name (default: <Name>Resource)", "CLASS" },
  26. { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Namespace for the generated class", "NAMESPACE" },
  27. { null }
  28. };
  29. public static int main(string[] args) {
  30. try {
  31. var opt_context = new OptionContext("INPUT_FILE - Generate a Statum ConstantStatumResource");
  32. opt_context.add_main_entries(options, null);
  33. opt_context.parse(ref args);
  34. } catch (OptionError e) {
  35. stderr.printf("Error: %s\n", e.message);
  36. return 1;
  37. }
  38. string? input_file = null;
  39. if (args.length > 1) {
  40. input_file = args[1];
  41. }
  42. if (input_file == null) {
  43. stderr.printf("Error: No input file specified.\n");
  44. return 1;
  45. }
  46. var name = resource_name_override ?? Path.get_basename((!)input_file);
  47. var class_name_base = resource_name_override ?? Path.get_basename((!)input_file);
  48. var output_path = output_file ?? (make_pascal_case(class_name_base) + "Resource.vala");
  49. var class_name = class_name_override ?? (make_pascal_case(class_name_base) + "Resource");
  50. try {
  51. var tool = new Mkres();
  52. var generated = tool.process((!)input_file, name, class_name, content_type_override, namespace_name);
  53. write_file(output_path, generated);
  54. stdout.printf("Generated: %s\n", output_path);
  55. return 0;
  56. } catch (Error e) {
  57. stderr.printf("Error: %s\n", e.message);
  58. return 1;
  59. }
  60. }
  61. public string process(string input_path, string name, string class_name, string? content_type_override, string? namespace_name) throws Error {
  62. var input_data = read_all(input_path);
  63. string content_type;
  64. if (content_type_override != null) {
  65. content_type = (!)content_type_override;
  66. } else {
  67. content_type = guess_content_type(name, input_data);
  68. }
  69. var hash_hex = compute_hash_hex(input_data);
  70. var encodings = compress_all(input_data);
  71. return emit_vala(class_name, name, content_type, hash_hex, encodings, namespace_name);
  72. }
  73. private EncodedData[] compress_all(uint8[] input_data) {
  74. var encodings = new EncodedData[0];
  75. var identity_data = new uint8[input_data.length];
  76. Memory.copy(identity_data, input_data, input_data.length);
  77. encodings += new EncodedData("identity", (owned)identity_data);
  78. var input_buffer = new ByteBuffer.from_byte_array(input_data);
  79. // GZip
  80. var gzip = new GzipCompressor(9).compress_buffer(input_buffer, null).to_array();
  81. if (gzip.length < input_data.length) {
  82. encodings += new EncodedData("gzip", (owned)gzip);
  83. }
  84. // Zstandard
  85. var zstd = new ZstdCompressor(19).compress_buffer(input_buffer, null).to_array();
  86. if (zstd.length < input_data.length) {
  87. encodings += new EncodedData("zstd", (owned)zstd);
  88. }
  89. // Brotli
  90. var br = new BrotliCompressor(11).compress_buffer(input_buffer, null).to_array();
  91. if (br.length < input_data.length) {
  92. encodings += new EncodedData("br", (owned)br);
  93. }
  94. return encodings;
  95. }
  96. private string emit_vala(string class_name, string name, string content_type, string hash_hex, EncodedData[] encodings, string? namespace_name) {
  97. var b = new StringBuilder();
  98. string i1 = namespace_name != null ? " " : "";
  99. string i2 = namespace_name != null ? " " : " ";
  100. string i3 = namespace_name != null ? " " : " ";
  101. string i4 = namespace_name != null ? " " : " ";
  102. b.append("using Statum;\n");
  103. b.append("using Invercargill;\n");
  104. b.append("using Invercargill.DataStructures;\n\n");
  105. if (namespace_name != null) {
  106. b.append_printf("namespace %s {\n\n", namespace_name);
  107. }
  108. b.append_printf("%s// Generated by statum-mkres\n", i1);
  109. b.append_printf("%spublic class %s : ConstantStatumResource {\n\n", i1, class_name);
  110. b.append_printf("%spublic override string name { get { return \"%s\"; } }\n", i2, escape(name));
  111. b.append_printf("%spublic override string file_hash { get { return \"%s\"; } }\n", i2, hash_hex);
  112. b.append_printf("%spublic override string content_type { get { return \"%s\"; } }\n\n", i2, escape(content_type));
  113. // Order by size so the client gets the smallest supported encoding.
  114. var ordered = sort_by_size(encodings);
  115. b.append_printf("%spublic override string get_best_encoding(Set<string> supported) {\n", i2);
  116. foreach (var encoding in ordered) {
  117. if (encoding.type == "identity") {
  118. continue;
  119. }
  120. b.append_printf("%sif (supported.has(\"%s\")) return \"%s\";\n", i3, encoding.type, encoding.type);
  121. }
  122. b.append_printf("%sreturn \"identity\";\n", i3);
  123. b.append_printf("%s}\n\n", i2);
  124. b.append_printf("%spublic override unowned uint8[] get_encoding(string encoding) {\n", i2);
  125. b.append_printf("%sswitch (encoding) {\n", i3);
  126. foreach (var encoding in encodings) {
  127. var var_name = encoding_const(encoding.type);
  128. b.append_printf("%scase \"%s\": return %s;\n", i4, encoding.type, var_name);
  129. }
  130. b.append_printf("%sdefault: return IDENTITY_DATA;\n", i4);
  131. b.append_printf("%s}\n", i3);
  132. b.append_printf("%s}\n\n", i2);
  133. foreach (var encoding in encodings) {
  134. var var_name = encoding_const(encoding.type);
  135. b.append_printf("%sprivate const uint8[] %s = {\n", i2, var_name);
  136. append_byte_array(b, encoding.data, i3);
  137. b.append_printf("\n%s};\n\n", i2);
  138. }
  139. b.append_printf("%s}\n", i1);
  140. if (namespace_name != null) {
  141. b.append("}\n");
  142. }
  143. return b.str;
  144. }
  145. private static EncodedData[] sort_by_size(EncodedData[] encodings) {
  146. var copy = new EncodedData[encodings.length];
  147. for (var i = 0; i < encodings.length; i++) {
  148. copy[i] = encodings[i];
  149. }
  150. for (var i = 0; i < copy.length - 1; i++) {
  151. for (var j = i + 1; j < copy.length; j++) {
  152. if (copy[i].data.length > copy[j].data.length) {
  153. var tmp = copy[i];
  154. copy[i] = copy[j];
  155. copy[j] = tmp;
  156. }
  157. }
  158. }
  159. return copy;
  160. }
  161. private static string encoding_const(string encoding_type) {
  162. return encoding_type.replace("-", "_").up() + "_DATA";
  163. }
  164. private static void append_byte_array(StringBuilder b, uint8[] data, string indent) {
  165. for (var i = 0; i < data.length; i++) {
  166. if (i == 0) {
  167. b.append(indent);
  168. } else if (i % 16 == 0) {
  169. b.append(",\n").append(indent);
  170. } else {
  171. b.append(", ");
  172. }
  173. b.append_printf("0x%02x", data[i]);
  174. }
  175. }
  176. private static string escape(string s) {
  177. return s.replace("\\", "\\\\").replace("\"", "\\\"");
  178. }
  179. private static string guess_content_type(string filename, uint8[] sample_data) {
  180. bool uncertain;
  181. var content_type = ContentType.guess(filename, sample_data, out uncertain);
  182. var mime = ContentType.get_mime_type(content_type);
  183. return mime ?? "application/octet-stream";
  184. }
  185. private static string compute_hash_hex(uint8[] data) {
  186. var checksum = new Checksum(ChecksumType.SHA512);
  187. checksum.update(data, data.length);
  188. return checksum.get_string();
  189. }
  190. private static uint8[] read_all(string path) throws Error {
  191. var file = File.new_for_path(path);
  192. if (!file.query_exists()) {
  193. throw new IOError.NOT_FOUND(@"Input file '$path' does not exist");
  194. }
  195. var size = (size_t) file.query_info("standard::size", 0).get_size();
  196. var stream = new DataInputStream(file.read());
  197. var bytes = stream.read_bytes(size);
  198. stream.close();
  199. var data = new uint8[bytes.get_size()];
  200. Memory.copy(data, bytes.get_data(), bytes.get_size());
  201. return data;
  202. }
  203. private static void write_file(string path, string contents) throws Error {
  204. var file = File.new_for_path(path);
  205. var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
  206. stream.put_string(contents);
  207. stream.close();
  208. }
  209. private static string make_pascal_case(string input) {
  210. var name = input;
  211. var dot = name.last_index_of(".");
  212. if (dot > 0) {
  213. name = name.substring(0, dot);
  214. }
  215. var result = new StringBuilder();
  216. var capitalize_next = true;
  217. foreach (var c in name.data) {
  218. if (c == '-' || c == '_' || c == ' ' || c == '.') {
  219. capitalize_next = true;
  220. } else {
  221. if (capitalize_next) {
  222. result.append_printf("%c", c >= 'a' && c <= 'z' ? c - 32 : c);
  223. capitalize_next = false;
  224. } else {
  225. result.append_printf("%c", c);
  226. }
  227. }
  228. }
  229. return result.str;
  230. }
  231. private class EncodedData {
  232. public string type;
  233. public uint8[] data;
  234. public EncodedData(string type, owned uint8[] data) {
  235. this.type = type;
  236. this.data = (owned)data;
  237. }
  238. }
  239. }
  240. }