using Astralis; using Invercargill; using Invercargill.DataStructures; namespace Statum.Tools { /** * `statum-mkres` — generates a {@link Statum.ConstantStatumResource} Vala * subclass from an input file, precompressing it with identity/gzip/zstd/br. * * Mirrors `spry-mkssr --vala`. Intended to be wired as a meson * `custom_target` (see `tools/meson.build`). * * statum-mkres -o LogoResource.vala -n logo.png logo.png * statum-mkres --ns MyApp.Static -c text/css -o Styles.vala styles.css */ public class Mkres : Object { private static string? output_file = null; private static string? content_type_override = null; private static string? resource_name_override = null; private static string? class_name_override = null; private static string? namespace_name = null; private const OptionEntry[] options = { { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output Vala file name (default: Resource.vala)", "FILE" }, { "content-type", 'c', 0, OptionArg.STRING, ref content_type_override, "Override content type (e.g. text/html)", "TYPE" }, { "name", 'n', 0, OptionArg.STRING, ref resource_name_override, "Resource name (default: input filename)", "NAME" }, { "class-name", '\0', 0, OptionArg.STRING, ref class_name_override, "Generated Vala class name (default: Resource)", "CLASS" }, { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Namespace for the generated class", "NAMESPACE" }, { null } }; public static int main(string[] args) { try { var opt_context = new OptionContext("INPUT_FILE - Generate a Statum ConstantStatumResource"); opt_context.add_main_entries(options, null); opt_context.parse(ref args); } catch (OptionError e) { stderr.printf("Error: %s\n", e.message); return 1; } string? input_file = null; if (args.length > 1) { input_file = args[1]; } if (input_file == null) { stderr.printf("Error: No input file specified.\n"); return 1; } var name = resource_name_override ?? Path.get_basename((!)input_file); var class_name_base = resource_name_override ?? Path.get_basename((!)input_file); var output_path = output_file ?? (make_pascal_case(class_name_base) + "Resource.vala"); var class_name = class_name_override ?? (make_pascal_case(class_name_base) + "Resource"); try { var tool = new Mkres(); var generated = tool.process((!)input_file, name, class_name, content_type_override, namespace_name); write_file(output_path, generated); stdout.printf("Generated: %s\n", output_path); return 0; } catch (Error e) { stderr.printf("Error: %s\n", e.message); return 1; } } public string process(string input_path, string name, string class_name, string? content_type_override, string? namespace_name) throws Error { var input_data = read_all(input_path); string content_type; if (content_type_override != null) { content_type = (!)content_type_override; } else { content_type = guess_content_type(name, input_data); } var hash_hex = compute_hash_hex(input_data); var encodings = compress_all(input_data); return emit_vala(class_name, name, content_type, hash_hex, encodings, namespace_name); } private EncodedData[] compress_all(uint8[] input_data) { var encodings = new EncodedData[0]; var identity_data = new uint8[input_data.length]; Memory.copy(identity_data, input_data, input_data.length); encodings += new EncodedData("identity", (owned)identity_data); var input_buffer = new ByteBuffer.from_byte_array(input_data); // GZip var gzip = new GzipCompressor(9).compress_buffer(input_buffer, null).to_array(); if (gzip.length < input_data.length) { encodings += new EncodedData("gzip", (owned)gzip); } // Zstandard var zstd = new ZstdCompressor(19).compress_buffer(input_buffer, null).to_array(); if (zstd.length < input_data.length) { encodings += new EncodedData("zstd", (owned)zstd); } // Brotli var br = new BrotliCompressor(11).compress_buffer(input_buffer, null).to_array(); if (br.length < input_data.length) { encodings += new EncodedData("br", (owned)br); } return encodings; } private string emit_vala(string class_name, string name, string content_type, string hash_hex, EncodedData[] encodings, string? namespace_name) { var b = new StringBuilder(); string i1 = namespace_name != null ? " " : ""; string i2 = namespace_name != null ? " " : " "; string i3 = namespace_name != null ? " " : " "; string i4 = namespace_name != null ? " " : " "; b.append("using Statum;\n"); b.append("using Invercargill;\n"); b.append("using Invercargill.DataStructures;\n\n"); if (namespace_name != null) { b.append_printf("namespace %s {\n\n", namespace_name); } b.append_printf("%s// Generated by statum-mkres\n", i1); b.append_printf("%spublic class %s : ConstantStatumResource {\n\n", i1, class_name); b.append_printf("%spublic override string name { get { return \"%s\"; } }\n", i2, escape(name)); b.append_printf("%spublic override string file_hash { get { return \"%s\"; } }\n", i2, hash_hex); b.append_printf("%spublic override string content_type { get { return \"%s\"; } }\n\n", i2, escape(content_type)); // Order by size so the client gets the smallest supported encoding. var ordered = sort_by_size(encodings); b.append_printf("%spublic override string get_best_encoding(Set supported) {\n", i2); foreach (var encoding in ordered) { if (encoding.type == "identity") { continue; } b.append_printf("%sif (supported.has(\"%s\")) return \"%s\";\n", i3, encoding.type, encoding.type); } b.append_printf("%sreturn \"identity\";\n", i3); b.append_printf("%s}\n\n", i2); b.append_printf("%spublic override unowned uint8[] get_encoding(string encoding) {\n", i2); b.append_printf("%sswitch (encoding) {\n", i3); foreach (var encoding in encodings) { var var_name = encoding_const(encoding.type); b.append_printf("%scase \"%s\": return %s;\n", i4, encoding.type, var_name); } b.append_printf("%sdefault: return IDENTITY_DATA;\n", i4); b.append_printf("%s}\n", i3); b.append_printf("%s}\n\n", i2); foreach (var encoding in encodings) { var var_name = encoding_const(encoding.type); b.append_printf("%sprivate const uint8[] %s = {\n", i2, var_name); append_byte_array(b, encoding.data, i3); b.append_printf("\n%s};\n\n", i2); } b.append_printf("%s}\n", i1); if (namespace_name != null) { b.append("}\n"); } return b.str; } private static EncodedData[] sort_by_size(EncodedData[] encodings) { var copy = new EncodedData[encodings.length]; for (var i = 0; i < encodings.length; i++) { copy[i] = encodings[i]; } for (var i = 0; i < copy.length - 1; i++) { for (var j = i + 1; j < copy.length; j++) { if (copy[i].data.length > copy[j].data.length) { var tmp = copy[i]; copy[i] = copy[j]; copy[j] = tmp; } } } return copy; } private static string encoding_const(string encoding_type) { return encoding_type.replace("-", "_").up() + "_DATA"; } private static void append_byte_array(StringBuilder b, uint8[] data, string indent) { for (var i = 0; i < data.length; i++) { if (i == 0) { b.append(indent); } else if (i % 16 == 0) { b.append(",\n").append(indent); } else { b.append(", "); } b.append_printf("0x%02x", data[i]); } } private static string escape(string s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); } private static string guess_content_type(string filename, uint8[] sample_data) { bool uncertain; var content_type = ContentType.guess(filename, sample_data, out uncertain); var mime = ContentType.get_mime_type(content_type); return mime ?? "application/octet-stream"; } private static string compute_hash_hex(uint8[] data) { var checksum = new Checksum(ChecksumType.SHA512); checksum.update(data, data.length); return checksum.get_string(); } private static uint8[] read_all(string path) throws Error { var file = File.new_for_path(path); if (!file.query_exists()) { throw new IOError.NOT_FOUND(@"Input file '$path' does not exist"); } var size = (size_t) file.query_info("standard::size", 0).get_size(); var stream = new DataInputStream(file.read()); var bytes = stream.read_bytes(size); stream.close(); var data = new uint8[bytes.get_size()]; Memory.copy(data, bytes.get_data(), bytes.get_size()); return data; } private static void write_file(string path, string contents) throws Error { var file = File.new_for_path(path); var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE)); stream.put_string(contents); stream.close(); } private static string make_pascal_case(string input) { var name = input; var dot = name.last_index_of("."); if (dot > 0) { name = name.substring(0, dot); } var result = new StringBuilder(); var capitalize_next = true; foreach (var c in name.data) { if (c == '-' || c == '_' || c == ' ' || c == '.') { capitalize_next = true; } else { if (capitalize_next) { result.append_printf("%c", c >= 'a' && c <= 'z' ? c - 32 : c); capitalize_next = false; } else { result.append_printf("%c", c); } } } return result.str; } private class EncodedData { public string type; public uint8[] data; public EncodedData(string type, owned uint8[] data) { this.type = type; this.data = (owned)data; } } } }