@@ -409,33 +409,40 @@ static bool ShouldTraceRegistryKey(const std::string& rawKey, const std::string&
409409
410410void SetImportMap (const std::string& json) {
411411 g_importMap.clear ();
412- // Minimal JSON parser for {"imports": {"key": "value", ...}} shape.
413- // We avoid pulling in a full JSON library; the import map is simple flat key-value.
414- size_t importsPos = json.find (" \" imports\" " );
415- if (importsPos == std::string::npos) return ;
416- size_t braceOpen = json.find (' {' , importsPos + 9 );
417- if (braceOpen == std::string::npos) return ;
418- size_t braceClose = json.find (' }' , braceOpen + 1 );
419- if (braceClose == std::string::npos) return ;
420-
421- std::string inner = json.substr (braceOpen + 1 , braceClose - braceOpen - 1 );
422- // Parse "key": "value" pairs
423- size_t pos = 0 ;
424- while (pos < inner.size ()) {
425- size_t keyStart = inner.find (' "' , pos);
426- if (keyStart == std::string::npos) break ;
427- size_t keyEnd = inner.find (' "' , keyStart + 1 );
428- if (keyEnd == std::string::npos) break ;
429- std::string key = inner.substr (keyStart + 1 , keyEnd - keyStart - 1 );
430-
431- size_t valStart = inner.find (' "' , keyEnd + 1 );
432- if (valStart == std::string::npos) break ;
433- size_t valEnd = inner.find (' "' , valStart + 1 );
434- if (valEnd == std::string::npos) break ;
435- std::string val = inner.substr (valStart + 1 , valEnd - valStart - 1 );
436-
437- g_importMap[key] = val;
438- pos = valEnd + 1 ;
412+ // The import map is a small, flat {"imports": {"specifier": "target", ...}}
413+ // object. Parse it with Foundation's JSON reader rather than a hand-rolled
414+ // scanner so escapes, nesting, and malformed input are handled correctly and
415+ // can't desync key/value pairing.
416+ @autoreleasepool {
417+ NSData * data = [NSData dataWithBytes: json.data () length: json.size ()];
418+ if (data == nil || data.length == 0 ) {
419+ return ;
420+ }
421+ NSError * err = nil ;
422+ id parsed = [NSJSONSerialization JSONObjectWithData: data
423+ options: kNilOptions
424+ error: &err];
425+ if (parsed == nil || ![parsed isKindOfClass: [NSDictionary class ]]) {
426+ if (IsScriptLoadingLogEnabled ()) {
427+ NSString * detail = err.localizedDescription ?: @" not an object" ;
428+ Log (@" [import-map] parse failed: %s " , [detail UTF8String ] ?: " unknown" );
429+ }
430+ return ;
431+ }
432+ id imports = [(NSDictionary *)parsed objectForKey: @" imports" ];
433+ if (![imports isKindOfClass: [NSDictionary class ]]) {
434+ return ; // no "imports" object → empty map, same as the prior parser
435+ }
436+ for (id key in (NSDictionary *)imports) {
437+ if (![key isKindOfClass: [NSString class ]]) continue ;
438+ id value = [(NSDictionary *)imports objectForKey: key];
439+ if (![value isKindOfClass: [NSString class ]]) continue ; // skip non-string targets
440+ const char * k = [(NSString *)key UTF8String ];
441+ const char * v = [(NSString *)value UTF8String ];
442+ if (k != nullptr && v != nullptr ) {
443+ g_importMap[std::string (k)] = std::string (v);
444+ }
445+ }
439446 }
440447 if (IsScriptLoadingLogEnabled ()) {
441448 Log (@" [import-map] loaded %lu entries" , (unsigned long )g_importMap.size ());
0 commit comments