1+ use flate2:: { Compression , read:: GzDecoder , write:: GzEncoder } ;
12use retrack_types:: trackers:: { TrackerDataRevision , TrackerDataValue } ;
23use serde_json:: Value as JsonValue ;
3- use std:: collections:: VecDeque ;
4+ use std:: {
5+ collections:: VecDeque ,
6+ io:: { Read , Write } ,
7+ } ;
48use time:: OffsetDateTime ;
59use uuid:: Uuid ;
610
11+ /// Minimum size (in bytes) of Postcard-encoded data before gzip compression is applied.
12+ /// Values smaller than this typically don't benefit from compression.
13+ const COMPRESSION_THRESHOLD : usize = 512 ;
14+
15+ /// Magic byte prefixed to gzip-compressed data blobs to distinguish them from legacy
16+ /// uncompressed Postcard data. Uncompressed Postcard blobs never start with this byte
17+ /// because the first Postcard byte is a varint-encoded Vec length (0x00 would mean an
18+ /// empty vec, which is rejected on read anyway).
19+ const GZIP_MAGIC : u8 = 0x00 ;
20+
721#[ derive( Debug , Eq , PartialEq , Clone ) ]
822pub ( super ) struct RawTrackerDataRevision {
923 pub id : Uuid ,
@@ -12,11 +26,41 @@ pub(super) struct RawTrackerDataRevision {
1226 pub created_at : OffsetDateTime ,
1327}
1428
29+ fn decompress_data ( data : & [ u8 ] ) -> anyhow:: Result < Vec < u8 > > {
30+ if data. first ( ) == Some ( & GZIP_MAGIC ) {
31+ let mut decoder = GzDecoder :: new ( & data[ 1 ..] ) ;
32+ let mut decompressed = Vec :: new ( ) ;
33+ decoder. read_to_end ( & mut decompressed) ?;
34+ Ok ( decompressed)
35+ } else {
36+ Ok ( data. to_vec ( ) )
37+ }
38+ }
39+
40+ fn compress_data ( postcard_bytes : Vec < u8 > ) -> anyhow:: Result < Vec < u8 > > {
41+ if postcard_bytes. len ( ) < COMPRESSION_THRESHOLD {
42+ return Ok ( postcard_bytes) ;
43+ }
44+
45+ let mut buf = Vec :: with_capacity ( postcard_bytes. len ( ) / 2 ) ;
46+ buf. push ( GZIP_MAGIC ) ;
47+ let mut encoder = GzEncoder :: new ( & mut buf, Compression :: default ( ) ) ;
48+ encoder. write_all ( & postcard_bytes) ?;
49+ encoder. finish ( ) ?;
50+
51+ if buf. len ( ) < postcard_bytes. len ( ) {
52+ Ok ( buf)
53+ } else {
54+ Ok ( postcard_bytes)
55+ }
56+ }
57+
1558impl TryFrom < RawTrackerDataRevision > for TrackerDataRevision {
1659 type Error = anyhow:: Error ;
1760
1861 fn try_from ( raw : RawTrackerDataRevision ) -> Result < Self , Self :: Error > {
19- let mut original_and_mods = postcard:: from_bytes :: < Vec < String > > ( & raw . data) ?
62+ let postcard_bytes = decompress_data ( & raw . data) ?;
63+ let mut original_and_mods = postcard:: from_bytes :: < Vec < String > > ( & postcard_bytes) ?
2064 . into_iter ( )
2165 . map ( |raw_value| serde_json:: from_str ( & raw_value) )
2266 . collect :: < Result < VecDeque < JsonValue > , _ > > ( ) ?;
@@ -41,15 +85,16 @@ impl TryFrom<&TrackerDataRevision> for RawTrackerDataRevision {
4185 type Error = anyhow:: Error ;
4286
4387 fn try_from ( item : & TrackerDataRevision ) -> Result < Self , Self :: Error > {
88+ let postcard_bytes = postcard:: to_stdvec (
89+ & ( & item. data )
90+ . into_iter ( )
91+ . map ( |value| value. to_string ( ) )
92+ . collect :: < Vec < _ > > ( ) ,
93+ ) ?;
4494 Ok ( Self {
4595 id : item. id ,
4696 tracker_id : item. tracker_id ,
47- data : postcard:: to_stdvec (
48- & ( & item. data )
49- . into_iter ( )
50- . map ( |value| value. to_string ( ) )
51- . collect :: < Vec < _ > > ( ) ,
52- ) ?,
97+ data : compress_data ( postcard_bytes) ?,
5398 created_at : item. created_at ,
5499 } )
55100 }
@@ -90,4 +135,82 @@ mod tests {
90135
91136 Ok ( ( ) )
92137 }
138+
139+ #[ test]
140+ fn small_data_is_not_compressed ( ) -> anyhow:: Result < ( ) > {
141+ let data_revision = TrackerDataRevision {
142+ id : uuid ! ( "00000000-0000-0000-0000-000000000001" ) ,
143+ tracker_id : uuid ! ( "00000000-0000-0000-0000-000000000002" ) ,
144+ data : TrackerDataValue :: new ( json ! ( "small" ) ) ,
145+ created_at : OffsetDateTime :: from_unix_timestamp ( 946720800 ) ?,
146+ } ;
147+
148+ let raw = RawTrackerDataRevision :: try_from ( & data_revision) ?;
149+ assert_ne ! ( raw. data. first( ) , Some ( & super :: GZIP_MAGIC ) ) ;
150+ assert_eq ! ( TrackerDataRevision :: try_from( raw) ?, data_revision) ;
151+
152+ Ok ( ( ) )
153+ }
154+
155+ #[ test]
156+ fn large_data_is_compressed_and_decompressed ( ) -> anyhow:: Result < ( ) > {
157+ let large_json = json ! ( {
158+ "content" : "x" . repeat( 2000 ) ,
159+ "nested" : { "array" : ( 0 ..100 ) . collect:: <Vec <_>>( ) }
160+ } ) ;
161+ let data_revision = TrackerDataRevision {
162+ id : uuid ! ( "00000000-0000-0000-0000-000000000001" ) ,
163+ tracker_id : uuid ! ( "00000000-0000-0000-0000-000000000002" ) ,
164+ data : TrackerDataValue :: new ( large_json) ,
165+ created_at : OffsetDateTime :: from_unix_timestamp ( 946720800 ) ?,
166+ } ;
167+
168+ let raw = RawTrackerDataRevision :: try_from ( & data_revision) ?;
169+ assert_eq ! ( raw. data. first( ) , Some ( & super :: GZIP_MAGIC ) ) ;
170+
171+ let postcard_bytes = postcard:: to_stdvec (
172+ & ( & data_revision. data )
173+ . into_iter ( )
174+ . map ( |v| v. to_string ( ) )
175+ . collect :: < Vec < _ > > ( ) ,
176+ ) ?;
177+ assert ! (
178+ raw. data. len( ) < postcard_bytes. len( ) ,
179+ "compressed ({}) should be smaller than uncompressed ({})" ,
180+ raw. data. len( ) ,
181+ postcard_bytes. len( )
182+ ) ;
183+
184+ assert_eq ! ( TrackerDataRevision :: try_from( raw) ?, data_revision) ;
185+
186+ Ok ( ( ) )
187+ }
188+
189+ #[ test]
190+ fn can_read_legacy_uncompressed_data ( ) -> anyhow:: Result < ( ) > {
191+ let data_revision = TrackerDataRevision {
192+ id : uuid ! ( "00000000-0000-0000-0000-000000000001" ) ,
193+ tracker_id : uuid ! ( "00000000-0000-0000-0000-000000000002" ) ,
194+ data : TrackerDataValue :: new ( json ! ( { "key" : "value" . repeat( 200 ) } ) ) ,
195+ created_at : OffsetDateTime :: from_unix_timestamp ( 946720800 ) ?,
196+ } ;
197+
198+ let postcard_bytes = postcard:: to_stdvec (
199+ & ( & data_revision. data )
200+ . into_iter ( )
201+ . map ( |v| v. to_string ( ) )
202+ . collect :: < Vec < _ > > ( ) ,
203+ ) ?;
204+
205+ let raw = RawTrackerDataRevision {
206+ id : data_revision. id ,
207+ tracker_id : data_revision. tracker_id ,
208+ data : postcard_bytes,
209+ created_at : data_revision. created_at ,
210+ } ;
211+
212+ assert_eq ! ( TrackerDataRevision :: try_from( raw) ?, data_revision) ;
213+
214+ Ok ( ( ) )
215+ }
93216}
0 commit comments