33
33
//! [`code_id`]: struct.SourceBundle.html#method.code_id
34
34
//! [`SourceBundle::debug_session`]: struct.SourceBundle.html#method.debug_session
35
35
//! [`SourceBundleWriter`]: struct.SourceBundleWriter.html
36
+ //!
37
+ //! ## Artifact Bundles
38
+ //!
39
+ //! Source bundles share the format with a related concept, called an "artifact bundle". Artifact
40
+ //! bundles are essentially source bundles but they typically contain sources referred to by
41
+ //! JavaScript source maps and source maps themselves. For instance in an artifact
42
+ //! bundle a file entry has a `url` and might carry `headers` or individual debug IDs
43
+ //! per source file.
36
44
37
45
use std:: borrow:: Cow ;
38
46
use std:: collections:: { BTreeMap , BTreeSet , HashMap } ;
@@ -46,7 +54,7 @@ use std::sync::Arc;
46
54
use lazycell:: LazyCell ;
47
55
use parking_lot:: Mutex ;
48
56
use regex:: Regex ;
49
- use serde:: { Deserialize , Serialize } ;
57
+ use serde:: { de , Deserialize , Serialize } ;
50
58
use thiserror:: Error ;
51
59
use zip:: { write:: FileOptions , ZipWriter } ;
52
60
@@ -145,7 +153,7 @@ where
145
153
}
146
154
147
155
/// The type of a [`SourceFileInfo`](struct.SourceFileInfo.html).
148
- #[ derive( Clone , Copy , Debug , Eq , Ord , PartialEq , PartialOrd , Serialize , Deserialize ) ]
156
+ #[ derive( Clone , Copy , Debug , Eq , Ord , PartialEq , PartialOrd , Serialize , Deserialize , Hash ) ]
149
157
#[ serde( rename_all = "snake_case" ) ]
150
158
pub enum SourceFileType {
151
159
/// Regular source file.
@@ -173,10 +181,30 @@ pub struct SourceFileInfo {
173
181
#[ serde( default , skip_serializing_if = "String::is_empty" ) ]
174
182
url : String ,
175
183
176
- #[ serde( default , skip_serializing_if = "BTreeMap::is_empty" ) ]
184
+ #[ serde(
185
+ default ,
186
+ skip_serializing_if = "BTreeMap::is_empty" ,
187
+ deserialize_with = "deserialize_headers"
188
+ ) ]
177
189
headers : BTreeMap < String , String > ,
178
190
}
179
191
192
+ /// Helper to ensure that header keys are normalized to lowercase
193
+ fn deserialize_headers < ' de , D > ( deserializer : D ) -> Result < BTreeMap < String , String > , D :: Error >
194
+ where
195
+ D : de:: Deserializer < ' de > ,
196
+ {
197
+ let rv: BTreeMap < String , String > = de:: Deserialize :: deserialize ( deserializer) ?;
198
+ if rv. is_empty ( ) || rv. keys ( ) . all ( |x| x. chars ( ) . all ( |c| c. is_ascii_lowercase ( ) ) ) {
199
+ Ok ( rv)
200
+ } else {
201
+ Ok ( rv
202
+ . into_iter ( )
203
+ . map ( |( k, v) | ( k. to_ascii_lowercase ( ) , v) )
204
+ . collect ( ) )
205
+ }
206
+ }
207
+
180
208
impl SourceFileInfo {
181
209
/// Creates default file information.
182
210
pub fn new ( ) -> Self {
@@ -226,14 +254,58 @@ impl SourceFileInfo {
226
254
227
255
/// Retrieves the specified header, if it exists.
228
256
pub fn header ( & self , header : & str ) -> Option < & str > {
229
- self . headers . get ( header) . map ( String :: as_str)
257
+ if header. chars ( ) . all ( |x| x. is_ascii_lowercase ( ) ) {
258
+ self . headers . get ( header) . map ( String :: as_str)
259
+ } else {
260
+ self . headers . iter ( ) . find_map ( |( k, v) | {
261
+ if k. eq_ignore_ascii_case ( header) {
262
+ Some ( v. as_str ( ) )
263
+ } else {
264
+ None
265
+ }
266
+ } )
267
+ }
230
268
}
231
269
232
270
/// Adds a custom attribute following header conventions.
271
+ ///
272
+ /// Header keys are converted to lowercase before writing as this is
273
+ /// the canonical format for headers however the file format does
274
+ /// support headers to be case insensitive and they will be lower cased
275
+ /// upon reading.
276
+ ///
277
+ /// Headers on files are primarily be used to add auxiliary information
278
+ /// to files. The following headers are known and processed:
279
+ ///
280
+ /// - `debug-id`: see [`debug_id`](Self::debug_id)
281
+ /// - `sourcemap` (and `x-sourcemap`): see [`source_mapping_url`](Self::source_mapping_url)
233
282
pub fn add_header ( & mut self , header : String , value : String ) {
283
+ let mut header = header;
284
+ if !header. chars ( ) . all ( |x| x. is_ascii_lowercase ( ) ) {
285
+ header = header. to_ascii_uppercase ( ) ;
286
+ }
234
287
self . headers . insert ( header, value) ;
235
288
}
236
289
290
+ /// The debug ID of this minified source or sourcemap if it has any.
291
+ ///
292
+ /// Files have a debug ID if they have a header with the key `debug-id`.
293
+ /// At present debug IDs in source bundles are only ever given to minified
294
+ /// source files.
295
+ pub fn debug_id ( & self ) -> Option < DebugId > {
296
+ self . header ( "debug-id" ) . and_then ( |x| x. parse ( ) . ok ( ) )
297
+ }
298
+
299
+ /// The source mapping URL of the given minified source.
300
+ ///
301
+ /// Files have a source mapping URL if they have a header with the
302
+ /// key `sourcemap` (or the `x-sourcemap` legacy header) as part the
303
+ /// source map specification.
304
+ pub fn source_mapping_url ( & self ) -> Option < & str > {
305
+ self . header ( "sourcemap" )
306
+ . or_else ( || self . header ( "x-sourcemap" ) )
307
+ }
308
+
237
309
/// Returns `true` if this instance does not carry any information.
238
310
pub fn is_empty ( & self ) -> bool {
239
311
self . path . is_empty ( ) && self . ty . is_none ( ) && self . headers . is_empty ( )
@@ -309,7 +381,6 @@ struct SourceBundleManifest {
309
381
pub files : BTreeMap < String , SourceFileInfo > ,
310
382
311
383
/// Arbitrary attributes to include in the bundle.
312
- #[ serde( flatten) ]
313
384
pub attributes : BTreeMap < String , String > ,
314
385
}
315
386
@@ -481,6 +552,7 @@ impl<'data> SourceBundle<'data> {
481
552
manifest : self . manifest . clone ( ) ,
482
553
archive : self . archive . clone ( ) ,
483
554
files_by_path : LazyCell :: new ( ) ,
555
+ files_by_debug_id : LazyCell :: new ( ) ,
484
556
} )
485
557
}
486
558
@@ -600,6 +672,7 @@ pub struct SourceBundleDebugSession<'data> {
600
672
manifest : Arc < SourceBundleManifest > ,
601
673
archive : Arc < Mutex < zip:: read:: ZipArchive < std:: io:: Cursor < & ' data [ u8 ] > > > > ,
602
674
files_by_path : LazyCell < HashMap < String , String > > ,
675
+ files_by_debug_id : LazyCell < HashMap < ( DebugId , SourceFileType ) , String > > ,
603
676
}
604
677
605
678
impl < ' data > SourceBundleDebugSession < ' data > {
@@ -615,28 +688,52 @@ impl<'data> SourceBundleDebugSession<'data> {
615
688
std:: iter:: empty ( )
616
689
}
617
690
618
- /// Create a reverse mapping of source paths to ZIP paths.
619
- fn get_files_by_path ( & self ) -> HashMap < String , String > {
620
- let files = & self . manifest . files ;
621
- let mut files_by_path = HashMap :: with_capacity ( files. len ( ) ) ;
691
+ /// Get a reverse mapping of source paths to ZIP paths.
692
+ fn files_by_path ( & self ) -> & HashMap < String , String > {
693
+ self . files_by_path . borrow_with ( || {
694
+ let files = & self . manifest . files ;
695
+ let mut files_by_path = HashMap :: with_capacity ( files. len ( ) ) ;
622
696
623
- for ( zip_path, file_info) in files {
624
- if !file_info. path . is_empty ( ) {
625
- files_by_path. insert ( file_info. path . clone ( ) , zip_path. clone ( ) ) ;
697
+ for ( zip_path, file_info) in files {
698
+ if !file_info. path . is_empty ( ) {
699
+ files_by_path. insert ( file_info. path . clone ( ) , zip_path. clone ( ) ) ;
700
+ }
626
701
}
627
- }
628
702
629
- files_by_path
703
+ files_by_path
704
+ } )
705
+ }
706
+
707
+ /// Get a reverse mapping of debug ID to ZIP paths.
708
+ fn files_by_debug_id ( & self ) -> & HashMap < ( DebugId , SourceFileType ) , String > {
709
+ self . files_by_debug_id . borrow_with ( || {
710
+ let files = & self . manifest . files ;
711
+ let mut files_by_debug_id = HashMap :: new ( ) ;
712
+
713
+ for ( zip_path, file_info) in files {
714
+ if let ( Some ( debug_id) , Some ( ty) ) = ( file_info. debug_id ( ) , file_info. ty ( ) ) {
715
+ files_by_debug_id. insert ( ( debug_id, ty) , zip_path. clone ( ) ) ;
716
+ }
717
+ }
718
+
719
+ files_by_debug_id
720
+ } )
630
721
}
631
722
632
723
/// Get the path of a file in this bundle by its logical path.
633
724
fn zip_path_by_source_path ( & self , path : & str ) -> Option < & str > {
634
- self . files_by_path
635
- . borrow_with ( || self . get_files_by_path ( ) )
725
+ self . files_by_path ( )
636
726
. get ( path)
637
727
. map ( |zip_path| zip_path. as_str ( ) )
638
728
}
639
729
730
+ /// Get the path of a file in this bundle by its Debug ID and source file type.
731
+ fn zip_path_by_debug_id ( & self , debug_id : DebugId , ty : SourceFileType ) -> Option < & str > {
732
+ self . files_by_debug_id ( )
733
+ . get ( & ( debug_id, ty) )
734
+ . map ( |zip_path| zip_path. as_str ( ) )
735
+ }
736
+
640
737
/// Get source by the path of a file in the bundle.
641
738
fn source_by_zip_path ( & self , zip_path : & str ) -> Result < Option < String > , SourceBundleError > {
642
739
let mut archive = self . archive . lock ( ) ;
@@ -660,6 +757,32 @@ impl<'data> SourceBundleDebugSession<'data> {
660
757
let content = self . source_by_zip_path ( zip_path) ?;
661
758
Ok ( content. map ( |opt| SourceCode :: Content ( Cow :: Owned ( opt) ) ) )
662
759
}
760
+
761
+ /// Looks up some source by debug ID and file type.
762
+ ///
763
+ /// Lookups by [`DebugId`] require knowledge of the file that is supposed to be
764
+ /// looked up as multiple files (one per type) can share the same debug ID.
765
+ /// Special care needs to be taken about [`SourceFileType::IndexedRamBundle`]
766
+ /// and [`SourceFileType::SourceMap`] which are different file types despite
767
+ /// the name of it.
768
+ ///
769
+ /// # Note on Abstractions
770
+ ///
771
+ /// This method is currently not exposed via a standardized debug session
772
+ /// as it's primarily used for the JavaScript processing system which uses
773
+ /// different abstractions.
774
+ pub fn source_by_debug_id (
775
+ & self ,
776
+ debug_id : DebugId ,
777
+ ty : SourceFileType ,
778
+ ) -> Result < Option < SourceCode < ' _ > > , SourceBundleError > {
779
+ let zip_path = match self . zip_path_by_debug_id ( debug_id, ty) {
780
+ Some ( zip_path) => zip_path,
781
+ None => return Ok ( None ) ,
782
+ } ;
783
+ let content = self . source_by_zip_path ( zip_path) ?;
784
+ Ok ( content. map ( |opt| SourceCode :: Content ( Cow :: Owned ( opt) ) ) )
785
+ }
663
786
}
664
787
665
788
impl < ' data , ' session > DebugSession < ' session > for SourceBundleDebugSession < ' data > {
@@ -1106,6 +1229,46 @@ mod tests {
1106
1229
Ok ( ( ) )
1107
1230
}
1108
1231
1232
+ #[ test]
1233
+ fn test_debug_id ( ) -> Result < ( ) , SourceBundleError > {
1234
+ let mut writer = Cursor :: new ( Vec :: new ( ) ) ;
1235
+ let mut bundle = SourceBundleWriter :: start ( & mut writer) ?;
1236
+
1237
+ let mut info = SourceFileInfo :: default ( ) ;
1238
+ info. set_ty ( SourceFileType :: MinifiedSource ) ;
1239
+ info. add_header (
1240
+ "debug-id" . into ( ) ,
1241
+ "5e618b9f-54a9-4389-b196-519819dd7c47" . into ( ) ,
1242
+ ) ;
1243
+ info. add_header ( "sourcemap" . into ( ) , "bar.js.min" . into ( ) ) ;
1244
+ bundle. add_file ( "bar.js" , & b"filecontents" [ ..] , info) ?;
1245
+ assert ! ( bundle. has_file( "bar.js" ) ) ;
1246
+
1247
+ bundle. finish ( ) ?;
1248
+ let bundle_bytes = writer. into_inner ( ) ;
1249
+ let bundle = SourceBundle :: parse ( & bundle_bytes) ?;
1250
+
1251
+ let sess = bundle. debug_session ( ) . unwrap ( ) ;
1252
+ let f = sess
1253
+ . source_by_debug_id (
1254
+ "5e618b9f-54a9-4389-b196-519819dd7c47" . parse ( ) . unwrap ( ) ,
1255
+ SourceFileType :: MinifiedSource ,
1256
+ )
1257
+ . unwrap ( )
1258
+ . expect ( "should exist" ) ;
1259
+ assert_eq ! ( f, SourceCode :: Content ( Cow :: Borrowed ( "filecontents" ) ) ) ;
1260
+
1261
+ assert ! ( sess
1262
+ . source_by_debug_id(
1263
+ "5e618b9f-54a9-4389-b196-519819dd7c47" . parse( ) . unwrap( ) ,
1264
+ SourceFileType :: Source
1265
+ )
1266
+ . unwrap( )
1267
+ . is_none( ) ) ;
1268
+
1269
+ Ok ( ( ) )
1270
+ }
1271
+
1109
1272
#[ test]
1110
1273
fn test_il2cpp_reference ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
1111
1274
let mut cpp_file = NamedTempFile :: new ( ) ?;
0 commit comments