From f5b041553958094eb2e22c7e6f14221014b83e3a Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Sat, 12 Apr 2025 21:06:21 +0200 Subject: [PATCH] gio: Add GZlibCompressor:os property The use case for exposing this field is GTK wanting reproducible encoding output across different OSes. I decided to expose the OS as an integer because zlib uses an int in its header and does not make its OS codes available as a custom type in its API. I also decided to limit it to values between 0 and 255 because zlib encodes the OS as a single byte. Test included. Fixes: #3663 --- gio/gzlibcompressor.c | 109 ++++++++++++++++++++++++++++++++++++++---- gio/gzlibcompressor.h | 6 +++ gio/tests/converter.c | 36 ++++++++++++++ 3 files changed, 142 insertions(+), 9 deletions(-) diff --git a/gio/gzlibcompressor.c b/gio/gzlibcompressor.c index 00bde8fe2..d95443f12 100644 --- a/gio/gzlibcompressor.c +++ b/gio/gzlibcompressor.c @@ -39,7 +39,8 @@ enum { PROP_0, PROP_FORMAT, PROP_LEVEL, - PROP_FILE_INFO + PROP_FILE_INFO, + PROP_OS }; /** @@ -60,6 +61,7 @@ struct _GZlibCompressor z_stream zstream; gz_header gzheader; GFileInfo *file_info; + int os; }; static void @@ -70,19 +72,27 @@ g_zlib_compressor_set_gzheader (GZlibCompressor *compressor) const gchar *filename; if (compressor->format != G_ZLIB_COMPRESSOR_FORMAT_GZIP || - compressor->file_info == NULL) + (compressor->file_info == NULL && + compressor->os < 0)) return; memset (&compressor->gzheader, 0, sizeof (gz_header)); - compressor->gzheader.os = 0x03; /* Unix */ - filename = g_file_info_get_name (compressor->file_info); - compressor->gzheader.name = (Bytef *) filename; - compressor->gzheader.name_max = filename ? strlen (filename) + 1 : 0; + if (compressor->os >= 0) + compressor->gzheader.os = compressor->os; + else + compressor->gzheader.os = 0x03; /* Unix */ - compressor->gzheader.time = - (uLong) g_file_info_get_attribute_uint64 (compressor->file_info, - G_FILE_ATTRIBUTE_TIME_MODIFIED); + if (compressor->file_info) + { + filename = g_file_info_get_name (compressor->file_info); + compressor->gzheader.name = (Bytef *) filename; + compressor->gzheader.name_max = filename ? strlen (filename) + 1 : 0; + + compressor->gzheader.time = + (uLong) g_file_info_get_attribute_uint64 (compressor->file_info, + G_FILE_ATTRIBUTE_TIME_MODIFIED); + } if (deflateSetHeader (&compressor->zstream, &compressor->gzheader) != Z_OK) g_warning ("unexpected zlib error: %s", compressor->zstream.msg); @@ -133,6 +143,10 @@ g_zlib_compressor_set_property (GObject *object, g_zlib_compressor_set_file_info (compressor, g_value_get_object (value)); break; + case PROP_OS: + g_zlib_compressor_set_os (compressor, g_value_get_int (value)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -164,6 +178,10 @@ g_zlib_compressor_get_property (GObject *object, g_value_set_object (value, compressor->file_info); break; + case PROP_OS: + g_value_set_int (value, compressor->os); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; @@ -173,6 +191,7 @@ g_zlib_compressor_get_property (GObject *object, static void g_zlib_compressor_init (GZlibCompressor *compressor) { + compressor->os = -1; } static void @@ -268,6 +287,31 @@ g_zlib_compressor_class_init (GZlibCompressorClass *klass) G_TYPE_FILE_INFO, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GZlibCompressor:os: + * + * The OS code of the gzip header. + * + * This will be used if set to a non-negative value, and if + * [property@Gio.ZlibCompressor:format] is + * [enum@Gio.ZlibCompressorFormat.GZIP], the compressor will set the OS code of + * the gzip header to this value. + * + * If the value is unset, zlib will set the OS code depending on the platform. + * This may be undesirable when reproducible output is desired. In that case setting + * the OS code to `3` (for Unix) is recommended. + * + * Since: 2.86 + */ + g_object_class_install_property (gobject_class, + PROP_OS, + g_param_spec_int ("os", NULL, NULL, + -1, 255, + -1, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY)); } /** @@ -348,6 +392,53 @@ g_zlib_compressor_set_file_info (GZlibCompressor *compressor, g_zlib_compressor_set_gzheader (compressor); } +/** + * g_zlib_compressor_get_os: + * @compressor: a compressor + * + * Gets the [property@Gio.ZlibCompressor:os] property. + * + * Returns: the previously set OS value, or `-1` if unset + * Since: 2.86 + */ +int +g_zlib_compressor_get_os (GZlibCompressor *compressor) +{ + g_return_val_if_fail (G_IS_ZLIB_COMPRESSOR (compressor), -1); + + return compressor->os; +} + +/** + * g_zlib_compressor_set_os: + * @compressor: a compressor + * @os: the OS code to use, or `-1` to unset + * + * Sets the [property@Gio.ZlibCompressor:os] property. + * + * Note: it is an error to call this function while a compression is in + * progress; it may only be called immediately after creation of @compressor, + * or after resetting it with [method@Gio.Converter.reset]. + * + * Since: 2.86 + */ +void +g_zlib_compressor_set_os (GZlibCompressor *compressor, + int os) +{ + g_return_if_fail (G_IS_ZLIB_COMPRESSOR (compressor)); + g_return_if_fail (os >= -1 && os < 256); + + if (os == compressor->os) + return; + + compressor->os = os; + + g_object_notify (G_OBJECT (compressor), "os"); + + g_zlib_compressor_set_gzheader (compressor); +} + static void g_zlib_compressor_reset (GConverter *converter) { diff --git a/gio/gzlibcompressor.h b/gio/gzlibcompressor.h index b8a99eaac..acb8efa48 100644 --- a/gio/gzlibcompressor.h +++ b/gio/gzlibcompressor.h @@ -59,6 +59,12 @@ GIO_AVAILABLE_IN_ALL void g_zlib_compressor_set_file_info (GZlibCompressor *compressor, GFileInfo *file_info); +GIO_AVAILABLE_IN_2_86 +int g_zlib_compressor_get_os (GZlibCompressor *compressor); +GIO_AVAILABLE_IN_2_86 +void g_zlib_compressor_set_os (GZlibCompressor *compressor, + int os); + G_END_DECLS #endif /* __G_ZLIB_COMPRESSOR_H__ */ diff --git a/gio/tests/converter.c b/gio/tests/converter.c index db7e0c3af..935cc3fbc 100644 --- a/gio/tests/converter.c +++ b/gio/tests/converter.c @@ -96,6 +96,41 @@ test_convert_bytes (void) g_bytes_unref (bytes); } +static void +test_gzip_os_property (void) +{ + const int os_testvalue = 42; + const char *data = "Hello World"; + GBytes *bytes; + GZlibCompressor *compressor; + GError *error = NULL; + GBytes *result; + int os; + const char *encoded; + + bytes = g_bytes_new_static (data, sizeof (data)); + + compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, 9); + g_zlib_compressor_set_os (compressor, os_testvalue); + g_assert_cmpint (g_zlib_compressor_get_os (compressor), ==, os_testvalue); + + g_object_get (compressor, "os", &os, NULL); + g_assert_cmpint (os, ==, os_testvalue); + + result = g_converter_convert_bytes (G_CONVERTER (compressor), bytes, &error); + g_assert_no_error (error); + g_assert_nonnull (result); + + /* The OS is stored in the 10th byte of the header, see RFC 1952 */ + g_assert_cmpint (g_bytes_get_size (result), >, 10); + encoded = g_bytes_get_data (result, NULL); + g_assert_cmpint (encoded[9], ==, os_testvalue); + + g_bytes_unref (result); + g_object_unref (compressor); + g_bytes_unref (bytes); +} + int main (int argc, char *argv[]) @@ -104,6 +139,7 @@ main (int argc, g_test_add_func ("/converter/bytes", test_convert_bytes); g_test_add_func ("/converter/extra-bytes-at-end", test_extra_bytes_at_end); + g_test_add_func ("/converter/gzip-os-property", test_gzip_os_property); return g_test_run(); }