@@ -88,6 +88,23 @@ struct VpnConfig {
8888 return str
8989 }
9090
91+ /// Android-compatible share string: `mhrv-rs://` + URL-safe base64 of the
92+ /// zlib-compressed (RFC 1950) JSON — byte-for-byte decodable by Android's
93+ /// ConfigStore.decode (InflaterInputStream). Falls back to uncompressed
94+ /// base64 if compression fails (still decodable by both sides).
95+ func encode( ) -> String {
96+ let json = toJson ( pretty: false )
97+ let bytes = Data ( json. utf8)
98+ let payload = Self . zlibDeflate ( bytes) ?? bytes
99+ return " mhrv-rs:// " + Self. urlSafeBase64 ( payload)
100+ }
101+
102+ private static func urlSafeBase64( _ data: Data ) -> String {
103+ data. base64EncodedString ( )
104+ . replacingOccurrences ( of: " + " , with: " - " )
105+ . replacingOccurrences ( of: " / " , with: " _ " )
106+ }
107+
91108 // MARK: parse
92109
93110 /// Try to parse raw text — mhrv-rs:// (Android share), JSON, or TOML.
@@ -108,10 +125,18 @@ struct VpnConfig {
108125 let rem = b64. count % 4
109126 if rem != 0 { b64 += String ( repeating: " = " , count: 4 - rem) }
110127 // ignoreUnknownCharacters handles any stray whitespace/newlines in the clipboard.
111- guard let compressed = Data ( base64Encoded: b64, options: . ignoreUnknownCharacters) ,
112- let inflated = zlibInflate ( compressed) ,
113- let json = String ( data: inflated, encoding: . utf8) else { return nil }
114- return fromJson ( json)
128+ guard let raw = Data ( base64Encoded: b64, options: . ignoreUnknownCharacters) else { return nil }
129+ // Prefer zlib-inflated JSON; fall back to raw UTF-8 (mirrors Android's
130+ // inflateOrRaw — handles uncompressed mhrv-rs:// payloads too).
131+ if let inflated = zlibInflate ( raw) ,
132+ let json = String ( data: inflated, encoding: . utf8) ,
133+ let cfg = fromJson ( json) {
134+ return cfg
135+ }
136+ if let json = String ( data: raw, encoding: . utf8) {
137+ return fromJson ( json)
138+ }
139+ return nil
115140 }
116141
117142 /// Decompress Java DeflaterOutputStream output (zlib / RFC 1950).
@@ -143,6 +168,41 @@ struct VpnConfig {
143168 return Data ( outBuf. prefix ( written) )
144169 }
145170
171+ /// Compress to zlib format (RFC 1950) so Android's InflaterInputStream reads
172+ /// it: 2-byte header + raw DEFLATE (Apple COMPRESSION_ZLIB) + 4-byte Adler-32.
173+ private static func zlibDeflate( _ data: Data ) -> Data ? {
174+ guard !data. isEmpty else { return nil }
175+ let cap = data. count + 4096
176+ var outBuf = [ UInt8] ( repeating: 0 , count: cap)
177+ var written = 0
178+ data. withUnsafeBytes { ( srcPtr: UnsafeRawBufferPointer ) in
179+ guard let src = srcPtr. bindMemory ( to: UInt8 . self) . baseAddress else { return }
180+ outBuf. withUnsafeMutableBufferPointer { dstPtr in
181+ written = compression_encode_buffer (
182+ dstPtr. baseAddress!, cap,
183+ src, srcPtr. count,
184+ nil , COMPRESSION_ZLIB
185+ )
186+ }
187+ }
188+ guard written > 0 else { return nil }
189+ var out = Data ( [ 0x78 , 0x9C ] ) // zlib header: deflate, default level
190+ out. append ( contentsOf: outBuf [ 0 ..< written] ) // raw DEFLATE body
191+ var adler = adler32 ( data) . bigEndian // Adler-32 over uncompressed bytes, MSB first
192+ withUnsafeBytes ( of: & adler) { out. append ( contentsOf: $0) }
193+ return out
194+ }
195+
196+ private static func adler32( _ data: Data ) -> UInt32 {
197+ var a : UInt32 = 1 , b : UInt32 = 0
198+ let mod : UInt32 = 65521
199+ for byte in data {
200+ a = ( a + UInt32( byte) ) % mod
201+ b = ( b + a) % mod
202+ }
203+ return ( b << 16 ) | a
204+ }
205+
146206 static func fromJson( _ json: String ) -> VpnConfig ? {
147207 guard let data = json. data ( using: . utf8) ,
148208 let obj = try ? JSONSerialization . jsonObject ( with: data) as? [ String : Any ] else {
@@ -341,7 +401,7 @@ private struct ConfigSharingBar: View {
341401 }
342402 . buttonStyle ( . bordered)
343403 . sheet ( isPresented: $showShareSheet) {
344- ShareSheet ( items: [ vpn. config. toJson ( ) ] )
404+ ShareSheet ( items: [ vpn. config. encode ( ) ] )
345405 }
346406 }
347407 }
0 commit comments