/* * Copyright (c) 2024 ETH Zürich, IT Services * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ using System; using System.IO; using System.IO.Compression; using SafeExamBrowser.Configuration.Contracts.DataCompression; using SafeExamBrowser.Logging.Contracts; namespace SafeExamBrowser.Configuration.DataCompression { /// /// Data compression using the GNU-Zip format (see https://en.wikipedia.org/wiki/Gzip). /// public class GZipCompressor : IDataCompressor { private const int ID1 = 0x1F; private const int ID2 = 0x8B; private const int CM = 8; private const int FOOTER_LENGTH = 8; private const int HEADER_LENGTH = 10; private ILogger logger; public GZipCompressor(ILogger logger) { this.logger = logger; } public Stream Compress(Stream data) { var compressed = new MemoryStream(); var originalSize = data.Length / 1000.0; logger.Debug($"Starting compression of '{data}' with {originalSize} KB data..."); data.Seek(0, SeekOrigin.Begin); using (var stream = new GZipStream(compressed, CompressionMode.Compress, true)) { data.CopyTo(stream); } logger.Debug($"Successfully compressed {originalSize} KB to {compressed.Length / 1000.0} KB data."); return compressed; } public Stream Decompress(Stream data) { var decompressed = new MemoryStream(); var originalSize = data.Length / 1000.0; logger.Debug($"Starting decompression of '{data}' with {originalSize} KB data..."); data.Seek(0, SeekOrigin.Begin); using (var stream = new GZipStream(data, CompressionMode.Decompress)) { stream.CopyTo(decompressed); } logger.Debug($"Successfully decompressed {originalSize} KB to {decompressed.Length / 1000.0} KB data."); return decompressed; } /// /// All gzip-compressed data has a 10-byte header and 8-byte footer. The header starts with two magic numbers (ID1 and ID2) and /// the used compression method (CM), which normally denotes the DEFLATE algorithm. See https://tools.ietf.org/html/rfc1952 for /// the original data format specification. /// public bool IsCompressed(Stream data) { try { var longEnough = data.Length > HEADER_LENGTH + FOOTER_LENGTH; data.Seek(0, SeekOrigin.Begin); if (longEnough) { var id1 = data.ReadByte(); var id2 = data.ReadByte(); var cm = data.ReadByte(); var compressed = id1 == ID1 && id2 == ID2 && cm == CM; return compressed; } logger.Debug($"'{data}' is not long enough ({data.Length} bytes) to be a gzip-compressed stream."); } catch (Exception e) { logger.Error($"Failed to check whether '{data}' with {data?.Length / 1000.0} KB data is a gzip-compressed stream!", e); } return false; } public byte[] Peek(Stream data, int count) { data.Seek(0, SeekOrigin.Begin); using (var stream = new GZipStream(data, CompressionMode.Decompress, true)) using (var decompressed = new MemoryStream()) { var buffer = new byte[count]; var bytesRead = stream.Read(buffer, 0, buffer.Length); decompressed.Write(buffer, 0, bytesRead); return decompressed.ToArray(); } } } }