statum-mkres.vala 12 KB

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