Browse Source

fix(app): persist defensiveness (#12973)

Adam 2 weeks ago
parent
commit
1e03a55acd

+ 5 - 0
packages/app/src/utils/persist.test.ts

@@ -99,4 +99,9 @@ describe("persist localStorage resilience", () => {
 
 
     expect(storage.getItem("direct-value")).toBe('{"value":5}')
     expect(storage.getItem("direct-value")).toBe('{"value":5}')
   })
   })
+
+  test("normalizer rejects malformed JSON payloads", () => {
+    const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
+    expect(result).toBeUndefined()
+  })
 })
 })

+ 31 - 30
packages/app/src/utils/persist.ts

@@ -195,6 +195,14 @@ function parse(value: string) {
   }
   }
 }
 }
 
 
+function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
+  const parsed = parse(raw)
+  if (parsed === undefined) return
+  const migrated = migrate ? migrate(parsed) : parsed
+  const merged = merge(defaults, migrated)
+  return JSON.stringify(merged)
+}
+
 function workspaceStorage(dir: string) {
 function workspaceStorage(dir: string) {
   const head = dir.slice(0, 12) || "workspace"
   const head = dir.slice(0, 12) || "workspace"
   const sum = checksum(dir) ?? "0"
   const sum = checksum(dir) ?? "0"
@@ -291,6 +299,7 @@ function localStorageDirect(): SyncStorage {
 export const PersistTesting = {
 export const PersistTesting = {
   localStorageDirect,
   localStorageDirect,
   localStorageWithPrefix,
   localStorageWithPrefix,
+  normalize,
 }
 }
 
 
 export const Persist = {
 export const Persist = {
@@ -358,12 +367,11 @@ export function persisted<T>(
         getItem: (key) => {
         getItem: (key) => {
           const raw = current.getItem(key)
           const raw = current.getItem(key)
           if (raw !== null) {
           if (raw !== null) {
-            const parsed = parse(raw)
-            if (parsed === undefined) return raw
-
-            const migrated = config.migrate ? config.migrate(parsed) : parsed
-            const merged = merge(defaults, migrated)
-            const next = JSON.stringify(merged)
+            const next = normalize(defaults, raw, config.migrate)
+            if (next === undefined) {
+              current.removeItem(key)
+              return null
+            }
             if (raw !== next) current.setItem(key, next)
             if (raw !== next) current.setItem(key, next)
             return next
             return next
           }
           }
@@ -372,16 +380,13 @@ export function persisted<T>(
             const legacyRaw = legacyStore.getItem(legacyKey)
             const legacyRaw = legacyStore.getItem(legacyKey)
             if (legacyRaw === null) continue
             if (legacyRaw === null) continue
 
 
-            current.setItem(key, legacyRaw)
+            const next = normalize(defaults, legacyRaw, config.migrate)
+            if (next === undefined) {
+              legacyStore.removeItem(legacyKey)
+              continue
+            }
+            current.setItem(key, next)
             legacyStore.removeItem(legacyKey)
             legacyStore.removeItem(legacyKey)
-
-            const parsed = parse(legacyRaw)
-            if (parsed === undefined) return legacyRaw
-
-            const migrated = config.migrate ? config.migrate(parsed) : parsed
-            const merged = merge(defaults, migrated)
-            const next = JSON.stringify(merged)
-            if (legacyRaw !== next) current.setItem(key, next)
             return next
             return next
           }
           }
 
 
@@ -405,12 +410,11 @@ export function persisted<T>(
       getItem: async (key) => {
       getItem: async (key) => {
         const raw = await current.getItem(key)
         const raw = await current.getItem(key)
         if (raw !== null) {
         if (raw !== null) {
-          const parsed = parse(raw)
-          if (parsed === undefined) return raw
-
-          const migrated = config.migrate ? config.migrate(parsed) : parsed
-          const merged = merge(defaults, migrated)
-          const next = JSON.stringify(merged)
+          const next = normalize(defaults, raw, config.migrate)
+          if (next === undefined) {
+            await current.removeItem(key).catch(() => undefined)
+            return null
+          }
           if (raw !== next) await current.setItem(key, next)
           if (raw !== next) await current.setItem(key, next)
           return next
           return next
         }
         }
@@ -421,16 +425,13 @@ export function persisted<T>(
           const legacyRaw = await legacyStore.getItem(legacyKey)
           const legacyRaw = await legacyStore.getItem(legacyKey)
           if (legacyRaw === null) continue
           if (legacyRaw === null) continue
 
 
-          await current.setItem(key, legacyRaw)
+          const next = normalize(defaults, legacyRaw, config.migrate)
+          if (next === undefined) {
+            await legacyStore.removeItem(legacyKey).catch(() => undefined)
+            continue
+          }
+          await current.setItem(key, next)
           await legacyStore.removeItem(legacyKey)
           await legacyStore.removeItem(legacyKey)
-
-          const parsed = parse(legacyRaw)
-          if (parsed === undefined) return legacyRaw
-
-          const migrated = config.migrate ? config.migrate(parsed) : parsed
-          const merged = merge(defaults, migrated)
-          const next = JSON.stringify(merged)
-          if (legacyRaw !== next) await current.setItem(key, next)
           return next
           return next
         }
         }
 
 

+ 10 - 1
packages/desktop/src/i18n/index.ts

@@ -116,6 +116,15 @@ function parseRecord(value: unknown) {
   return value as Record<string, unknown>
   return value as Record<string, unknown>
 }
 }
 
 
+function parseStored(value: unknown) {
+  if (typeof value !== "string") return value
+  try {
+    return JSON.parse(value) as unknown
+  } catch {
+    return value
+  }
+}
+
 function pickLocale(value: unknown): Locale | null {
 function pickLocale(value: unknown): Locale | null {
   const direct = parseLocale(value)
   const direct = parseLocale(value)
   if (direct) return direct
   if (direct) return direct
@@ -169,7 +178,7 @@ export function initI18n(): Promise<Locale> {
     if (!store) return state.locale
     if (!store) return state.locale
 
 
     const raw = await store.get("language").catch(() => null)
     const raw = await store.get("language").catch(() => null)
-    const value = typeof raw === "string" ? JSON.parse(raw) : raw
+    const value = parseStored(raw)
     const next = pickLocale(value) ?? state.locale
     const next = pickLocale(value) ?? state.locale
 
 
     state.locale = next
     state.locale = next