BrotliCompressor.vala 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. namespace Astralis {
  4. public class BrotliCompressor : Compressor {
  5. private int _quality = 9;
  6. public BrotliCompressor(int quality = 9, uint64 max_buffer_size = 1024 * 1024 * 16) {
  7. base(max_buffer_size);
  8. this._quality = quality.clamp(Brotli.MIN_QUALITY, Brotli.MAX_QUALITY);
  9. }
  10. public override string encoding_token { get { return "br"; } }
  11. /// Picks an EncoderMode based on MIME type for optimal compression
  12. private static Brotli.EncoderMode pick_encoder_mode(string? content_type) {
  13. if (content_type == null) {
  14. return Brotli.EncoderMode.GENERIC;
  15. }
  16. string mime = content_type.down();
  17. // Text mode for text-based content types
  18. if (mime.has_prefix("text/")) {
  19. return Brotli.EncoderMode.TEXT;
  20. }
  21. // Text mode for application text formats
  22. if (mime.has_prefix("application/json") ||
  23. mime.has_prefix("application/xml") ||
  24. mime.has_prefix("application/javascript") ||
  25. mime.has_prefix("application/x-javascript") ||
  26. mime.has_prefix("application/xhtml+xml")) {
  27. return Brotli.EncoderMode.TEXT;
  28. }
  29. // Font mode for web fonts
  30. if (mime.has_prefix("font/") ||
  31. mime.has_prefix("application/font-") ||
  32. mime.has_prefix("application/x-font-") ||
  33. mime.has_suffix("woff") ||
  34. mime.has_suffix("woff2") ||
  35. mime.has_suffix("ttf") ||
  36. mime.has_suffix("otf") ||
  37. mime.has_suffix("eot")) {
  38. return Brotli.EncoderMode.FONT;
  39. }
  40. // Default to generic for everything else
  41. return Brotli.EncoderMode.GENERIC;
  42. }
  43. public override ByteBuffer compress_buffer(ByteBuffer data, string? content_type) throws Error {
  44. // Get maximum compressed size
  45. size_t max_output_size = Brotli.encoder_max_compressed_size((size_t) data.length);
  46. if (max_output_size == 0) {
  47. throw new IOError.FAILED("Brotli Error");
  48. }
  49. // Prepare input
  50. uint8[] input_bytes = data.to_array();
  51. // Allocate output buffer
  52. var output_buffer = new uint8[max_output_size];
  53. size_t encoded_size = max_output_size;
  54. // Pick encoder mode based on content type
  55. var mode = pick_encoder_mode(content_type);
  56. // Compress in one shot (using pointers for the C API)
  57. Brotli.Bool result = Brotli.encoder_compress(
  58. _quality,
  59. Brotli.DEFAULT_WINDOW,
  60. mode,
  61. input_bytes.length,
  62. input_bytes,
  63. ref encoded_size,
  64. output_buffer
  65. );
  66. if (result != Brotli.TRUE) {
  67. throw new IOError.FAILED("Brotli Error");
  68. }
  69. // Return compressed data (base class handles size comparison)
  70. return new ByteBuffer.from_byte_array(output_buffer[0:encoded_size]);
  71. }
  72. public override HttpResult compress_chunked(HttpResult inner_result) {
  73. // Get content type for compression optimization
  74. string? content_type = null;
  75. if (inner_result.headers.has("Content-Type")) {
  76. content_type = inner_result.headers["Content-Type"];
  77. }
  78. var mode = pick_encoder_mode(content_type);
  79. var streaming_result = new StreamingBrotliResult(inner_result, _quality, mode);
  80. return streaming_result;
  81. }
  82. /// Streaming compression result that compresses data on-the-fly using brotli
  83. private class StreamingBrotliResult : HttpResult {
  84. private HttpResult inner_result;
  85. private int quality;
  86. private Brotli.EncoderMode mode;
  87. public StreamingBrotliResult(HttpResult result, int quality, Brotli.EncoderMode mode) {
  88. base(result.status, null); // No content length for streaming
  89. copy_headers(result, this, {"content-length", "content-encoding"});
  90. this.inner_result = result;
  91. this.quality = quality;
  92. this.mode = mode;
  93. }
  94. public async override void send_body(AsyncOutput output) throws Error {
  95. // Create a brotli output stream wrapper that compresses on-the-fly
  96. var brotli_output = new BrotliAsyncOutput(output, quality, mode);
  97. // Send the body through the brotli wrapper
  98. yield inner_result.send_body(brotli_output);
  99. // Finish the compression stream
  100. yield brotli_output.finish();
  101. }
  102. }
  103. /// An AsyncOutput that compresses data on-the-fly using brotli format
  104. private class BrotliAsyncOutput : Object, AsyncOutput {
  105. private AsyncOutput downstream;
  106. private Brotli.EncoderState encoder;
  107. private bool finished = false;
  108. private uint8[] output_buffer;
  109. public bool connected { get { return downstream.connected; } }
  110. public bool write_would_block { get { return downstream.write_would_block; } }
  111. public BrotliAsyncOutput(AsyncOutput downstream, int quality, Brotli.EncoderMode mode) throws Error {
  112. this.downstream = downstream;
  113. // Initialize brotli encoder
  114. this.encoder = new Brotli.EncoderState(null, null, null);
  115. // Set encoder parameters
  116. this.encoder.set_parameter(Brotli.EncoderParameter.MODE, (uint32) mode);
  117. this.encoder.set_parameter(Brotli.EncoderParameter.QUALITY, (uint32) quality);
  118. this.encoder.set_parameter(Brotli.EncoderParameter.LGWIN, (uint32) Brotli.DEFAULT_WINDOW);
  119. // Allocate output buffer (16KB should be plenty for compressed chunks)
  120. this.output_buffer = new uint8[16384];
  121. }
  122. public async void write(BinaryData data) throws Error {
  123. if (!connected) {
  124. throw new IOError.CLOSED("Cannot write to closed output stream");
  125. }
  126. if (finished) {
  127. throw new IOError.FAILED("Cannot write to finished brotli stream");
  128. }
  129. uint8[] input_bytes = data.to_array();
  130. if (input_bytes.length == 0) {
  131. return;
  132. }
  133. // Set up input
  134. size_t available_in = input_bytes.length;
  135. uint8* next_in = input_bytes;
  136. // Compress with PROCESS operation (don't flush yet)
  137. while (available_in > 0) {
  138. size_t available_out = output_buffer.length;
  139. uint8* next_out = output_buffer;
  140. size_t total_out = 0;
  141. Brotli.Bool result = encoder.compress_stream(
  142. Brotli.EncoderOperation.PROCESS,
  143. ref available_in,
  144. ref next_in,
  145. ref available_out,
  146. ref next_out,
  147. out total_out
  148. );
  149. if (result != Brotli.TRUE) {
  150. throw new IOError.FAILED("Brotli compression stream error");
  151. }
  152. // Calculate how much compressed data was produced
  153. size_t compressed_size = output_buffer.length - available_out;
  154. if (compressed_size > 0) {
  155. yield downstream.write(new ByteBuffer.from_byte_array(output_buffer[0:compressed_size]));
  156. }
  157. }
  158. // Flush any pending output
  159. yield flush_pending();
  160. }
  161. public async void write_stream(InputStream stream) throws Error {
  162. uint8[] chunk = new uint8[8192];
  163. while (true) {
  164. ssize_t bytes_read = yield stream.read_async(chunk);
  165. if (bytes_read <= 0) {
  166. break;
  167. }
  168. yield write(new ByteBuffer.from_byte_array(chunk[0:bytes_read]));
  169. }
  170. }
  171. /// Flush any pending output from the encoder
  172. private async void flush_pending() throws Error {
  173. // Check if there's more output available
  174. while (encoder.has_more_output() == Brotli.TRUE) {
  175. size_t size = output_buffer.length;
  176. unowned uint8* taken = encoder.take_output(ref size);
  177. if (size > 0) {
  178. // Copy data from taken pointer to array
  179. uint8[] data = new uint8[size];
  180. Memory.copy(data, taken, size);
  181. yield downstream.write(new ByteBuffer.from_byte_array(data));
  182. }
  183. }
  184. }
  185. /// Finish the compression stream
  186. public async void finish() throws Error {
  187. if (finished) {
  188. return;
  189. }
  190. finished = true;
  191. // Finish the stream
  192. while (encoder.is_finished() != Brotli.TRUE) {
  193. size_t available_in = 0;
  194. uint8* next_in = null;
  195. size_t available_out = output_buffer.length;
  196. uint8* next_out = output_buffer;
  197. size_t total_out = 0;
  198. Brotli.Bool result = encoder.compress_stream(
  199. Brotli.EncoderOperation.FINISH,
  200. ref available_in,
  201. ref next_in,
  202. ref available_out,
  203. ref next_out,
  204. out total_out
  205. );
  206. if (result != Brotli.TRUE) {
  207. throw new IOError.FAILED("Brotli compression stream error during finish");
  208. }
  209. // Write any compressed data produced
  210. size_t compressed_size = output_buffer.length - available_out;
  211. if (compressed_size > 0) {
  212. yield downstream.write(new ByteBuffer.from_byte_array(output_buffer[0:compressed_size]));
  213. }
  214. // Also take any remaining output
  215. yield flush_pending();
  216. }
  217. // Encoder will be cleaned up by the free_function when the object is finalized
  218. }
  219. }
  220. }
  221. }