Wrap all JSON.parse calls with try-catch

This commit is contained in:
Misode
2024-10-26 21:49:12 +02:00
parent 6555e80ead
commit 2ff59b8405
19 changed files with 65 additions and 58 deletions
+8 -5
View File
@@ -4,6 +4,7 @@ import type { Project } from './contexts/index.js'
import { DRAFT_PROJECT } from './contexts/index.js'
import type { VersionId } from './services/index.js'
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
import { safeJsonParse } from './Utils.js'
export namespace Store {
export const ID_LANGUAGE = 'language'
@@ -65,7 +66,7 @@ export namespace Store {
export function getProjects(): Project[] {
const projects = localStorage.getItem(ID_PROJECTS)
if (projects) {
return JSON.parse(projects) as Project[]
return safeJsonParse(projects) ?? []
}
return [DRAFT_PROJECT]
}
@@ -73,13 +74,13 @@ export namespace Store {
export function getPreviewPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PREVIEW_PANEL_OPEN)
if (open === null) return undefined
return JSON.parse(open)
return safeJsonParse(open)
}
export function getProjectPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PROJECT_PANEL_OPEN)
if (open === null) return undefined
return JSON.parse(open)
return safeJsonParse(open)
}
export function getOpenProject() {
@@ -99,7 +100,8 @@ export namespace Store {
}
export function getGeneratorHistory(): string[] {
return JSON.parse(localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]')
const value = localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]'
return safeJsonParse(value) ?? []
}
export function setLanguage(language: string | undefined) {
@@ -173,7 +175,8 @@ export namespace Store {
}
export function getWhatsNewSeen(): { id: string, time: string }[] {
return JSON.parse(localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]')
const value = localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]'
return safeJsonParse(value) ?? []
}
export function seeWhatsNew(ids: string[]) {
+8
View File
@@ -623,3 +623,11 @@ export function makeDescriptionId(prefix: string, id: Identifier | undefined) {
}
return `${prefix}.${id.namespace}.${id.path.replaceAll('/', '.')}`
}
export function safeJsonParse(text: string): any {
try {
return JSON.parse(text)
} catch (e) {
return undefined
}
}
@@ -2,6 +2,7 @@ import type { DocAndNode } from '@spyglassmc/core'
import { useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { safeJsonParse } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
@@ -29,8 +30,11 @@ export function FileCreation({ docAndNode, id, method, onClose }: Props) {
return
}
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
const data = JSON.parse(docAndNode.doc.getText())
updateFile(id, undefined, { type: id, id: fileId, data })
const text = docAndNode.doc.getText()
const data = safeJsonParse(text)
if (data !== undefined) {
updateFile(id, undefined, { type: id, id: fileId, data })
}
onClose()
}
@@ -2,6 +2,7 @@ import type { DocAndNode } from '@spyglassmc/core'
import { useDocAndNode } from '../../contexts/Spyglass.jsx'
import { useVersion } from '../../contexts/Version.jsx'
import { checkVersion } from '../../services/index.js'
import { safeJsonParse } from '../../Utils.js'
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
export const HasPreview = ['loot_table', 'recipe', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'docAndNode']
@@ -27,7 +28,7 @@ export function PreviewPanel({ docAndNode: original, id, shown }: PreviewPanelPr
return <RecipePreview {...{ docAndNode, shown }} />
}
if (id === 'dimension' && JSON.parse(docAndNode.doc.getText()).generator?.type?.endsWith('noise')) {
if (id === 'dimension' && safeJsonParse(docAndNode.doc.getText())?.generator?.type?.endsWith('noise')) {
return <BiomeSourcePreview {...{ docAndNode, shown }} />
}
@@ -9,7 +9,7 @@ import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../h
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
import { Store } from '../../Store.js'
import { cleanUrl, genPath } from '../../Utils.js'
import { cleanUrl, genPath, safeJsonParse } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
export const SHARE_KEY = 'share'
@@ -102,8 +102,10 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setSharedSnippetId(undefined, true)
}
if (file) {
const data = JSON.parse(doc.getText())
updateFile(gen.id, file.id, { id: file.id, data })
const data = safeJsonParse(doc.getText())
if (data !== undefined) {
updateFile(gen.id, file.id, { id: file.id, data })
}
}
ignoreChange.current = false
setError(null)
@@ -211,7 +213,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setShareShown(true)
} else if (doc) {
setShareLoading(true)
shareSnippet(gen.id, version, JSON.parse(doc.getText()), previewShown)
shareSnippet(gen.id, version, doc.getText(), previewShown)
.then(({ id, length, compressed, rate }) => {
Analytics.createSnippet(gen.id, id, version, length, compressed, rate)
const url = `${location.origin}/${gen.url}/?${SHARE_KEY}=${id}`
@@ -5,7 +5,7 @@ import { getProjectData, useLocale, useProject, useStore, useVersion } from '../
import { useAsync } from '../../hooks/index.js'
import { checkVersion } from '../../services/Versions.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -32,7 +32,7 @@ export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => {
const [focused2, setFocused2] = useState<string[]>([])
const text = docAndNode.doc.getText()
const data = JSON.parse(text)
const data = safeJsonParse(text) ?? {}
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
const hasRandomness = type === 'multi_noise' || type === 'the_end'
@@ -5,6 +5,7 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
@@ -18,7 +19,7 @@ export const BlockStatePreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
const definition = BlockDefinition.fromJson(JSON.parse(text))
const definition = BlockDefinition.fromJson(safeJsonParse(text) ?? {})
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
if (id.equals(PREVIEW_ID)) return definition
@@ -2,7 +2,7 @@ import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { PlacedFeature, PlacementContext } from './Decorator.js'
import { decorateChunk } from './Decorator.js'
@@ -50,7 +50,7 @@ export const DecoratorPreview = ({ docAndNode, shown }: PreviewProps) => {
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
const data = JSON.parse(text)
const data = safeJsonParse(text) ?? {}
iterateWorld2D(imageData.current, transform, (x, y) => {
const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16))
const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context))
@@ -6,7 +6,7 @@ import { getProjectData, useLocale, useProject, useVersion } from '../../context
import { useAsync } from '../../hooks/useAsync.js'
import { useLocalStorage } from '../../hooks/useLocalStorage.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -33,7 +33,7 @@ export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: df } = useAsync(async () => {
await DEEPSLATE.loadVersion(version, getProjectData(project))
const df = DEEPSLATE.loadDensityFunction(JSON.parse(text), minY, height, seed)
const df = DEEPSLATE.loadDensityFunction(safeJsonParse(text) ?? {}, minY, height, seed)
return df
}, [version, project, minY, height, seed, text])
@@ -3,7 +3,7 @@ import { useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { checkVersion, fetchAllPresets, fetchItemComponents } from '../../services/index.js'
import { clamp, jsonToNbt, randomSeed } from '../../Utils.js'
import { clamp, jsonToNbt, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { ItemDisplay1204 } from '../ItemDisplay1204.jsx'
@@ -40,10 +40,7 @@ export const LootTablePreview = ({ docAndNode }: PreviewProps) => {
return []
}
const [itemTags, lootTables, itemComponents, enchantments, enchantmentTags] = dependencies
let table = {}
try {
table = JSON.parse(text)
} catch (e) {}
const table = safeJsonParse(text) ?? {}
if (use1204) {
return generateLootTable1204(table, {
version, seed, luck, daytime, weather,
+2 -1
View File
@@ -5,6 +5,7 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
@@ -19,7 +20,7 @@ export const ModelPreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
const blockModel = BlockModel.fromJson(JSON.parse(text))
const blockModel = BlockModel.fromJson(safeJsonParse(text) ?? {})
blockModel.flatten(resources)
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
+2 -2
View File
@@ -3,7 +3,7 @@ import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -19,7 +19,7 @@ export const NoisePreview = ({ docAndNode, shown }: PreviewProps) => {
const noise = useMemo(() => {
const random = XoroshiroRandom.create(seed)
const params = NoiseParameters.fromJson(JSON.parse(text))
const params = NoiseParameters.fromJson(safeJsonParse(text) ?? {})
return new NormalNoise(random, params)
}, [text, seed])
@@ -6,7 +6,7 @@ import { getProjectData, useLocale, useProject, useVersion } from '../../context
import { useAsync } from '../../hooks/index.js'
import { fetchRegistries } from '../../services/index.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnInput, BtnMenu, ErrorPanel } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -26,7 +26,7 @@ export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => {
const text = docAndNode.doc.getText()
const { value, error } = useAsync(async () => {
const data = JSON.parse(text)
const data = safeJsonParse(text) ?? {}
await DEEPSLATE.loadVersion(version, getProjectData(project))
const biomeSource = { type: 'fixed', biome }
await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed)
@@ -4,7 +4,7 @@ import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchAllPresets } from '../../services/index.js'
import { jsonToNbt } from '../../Utils.js'
import { jsonToNbt, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import type { PreviewProps } from './index.js'
@@ -30,13 +30,13 @@ export const RecipePreview = ({ docAndNode }: PreviewProps) => {
}, [])
const text = docAndNode.doc.getText()
const recipe = JSON.parse(text)
const recipe = safeJsonParse(text) ?? {}
const items = useMemo<Map<Slot, ItemStack>>(() => {
return placeItems(version, recipe, animation, itemTags ?? new Map())
}, [text, animation, itemTags])
const gui = useMemo(() => {
const type = recipe.type?.replace(/^minecraft:/, '')
const type = recipe?.type?.replace(/^minecraft:/, '')
if (type === 'smelting' || type === 'blasting' || type === 'smoking' || type === 'campfire_cooking') {
return '/images/furnace.png'
} else if (type === 'stonecutting') {
@@ -5,7 +5,7 @@ import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import type { Color } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn } from '../index.js'
import { featureColors } from './Decorator.js'
import { DEEPSLATE } from './Deepslate.js'
@@ -21,7 +21,7 @@ export const StructureSetPreview = ({ docAndNode, shown }: PreviewProps) => {
const { value: structureSet } = useAsync(async () => {
await DEEPSLATE.loadVersion(version)
const structureSet = DEEPSLATE.loadStructureSet(JSON.parse(text), seed)
const structureSet = DEEPSLATE.loadStructureSet(safeJsonParse(text) ?? {}, seed)
return structureSet
}, [text, version, seed])
+2 -1
View File
@@ -3,6 +3,7 @@ import { createContext } from 'preact'
import { useCallback, useContext } from 'preact/hooks'
import { useLocalStorage } from '../hooks/index.js'
import type { Color } from '../Utils.js'
import { safeJsonParse } from '../Utils.js'
interface Store {
biomeColors: Record<string, [number, number, number]>
@@ -19,7 +20,7 @@ export function useStore() {
}
export function StoreProvider({ children }: { children: ComponentChildren }) {
const [biomeColors, setBiomeColors] = useLocalStorage<Record<string, Color>>('misode_biome_colors', {}, JSON.parse, JSON.stringify)
const [biomeColors, setBiomeColors] = useLocalStorage<Record<string, Color>>('misode_biome_colors', {}, s => safeJsonParse(s) ?? {}, JSON.stringify)
const setBiomeColor = useCallback((biome: string, color: Color) => {
setBiomeColors({...biomeColors, [biome]: color })
+4 -15
View File
@@ -1,5 +1,6 @@
import type { NbtTag } from 'deepslate'
import { Identifier, ItemStack } from 'deepslate'
import { safeJsonParse } from '../Utils.js'
export class ResolvedItem extends ItemStack {
@@ -108,11 +109,7 @@ export class ResolvedItem extends ItemStack {
public getLore() {
return this.get('lore', tag => {
return tag.isList() ? tag.map(e => {
try {
return JSON.parse(e.getAsString())
} catch (e) {
return { text: '(invalid lore line)' }
}
return safeJsonParse(e.getAsString()) ?? { text: '(invalid lore line)' }
}) : []
}) ?? []
}
@@ -153,11 +150,7 @@ export class ResolvedItem extends ItemStack {
public getHoverName() {
const customName = this.get('custom_name', tag => tag.isString() ? tag.getAsString() : undefined)
if (customName) {
try {
return JSON.parse(customName)
} catch (e) {
return '(invalid custom name)'
}
return safeJsonParse(customName) ?? '(invalid custom name)'
}
const bookTitle = this.get('written_book_content', tag => tag.isCompound() ? (tag.hasCompound('title') ? tag.getCompound('title').getString('raw') : tag.getString('title')) : undefined)
@@ -167,11 +160,7 @@ export class ResolvedItem extends ItemStack {
const itemName = this.get('item_name', tag => tag.isString() ? tag.getAsString() : undefined)
if (itemName) {
try {
return JSON.parse(itemName)
} catch (e) {
return { text: '(invalid item name)' }
}
return safeJsonParse(itemName) ?? { text: '(invalid item name)' }
}
const guess = this.id.path
+5 -5
View File
@@ -1,14 +1,14 @@
import lz from 'lz-string'
import { safeJsonParse } from '../Utils.js'
import type { VersionId } from './Versions.js'
const API_PREFIX = 'https://snippets.misode.workers.dev'
const ShareCache = new Map<string, string>()
export async function shareSnippet(type: string, version: VersionId, jsonData: any, show_preview: boolean) {
export async function shareSnippet(type: string, version: VersionId, text: string, show_preview: boolean) {
try {
const raw = JSON.stringify(jsonData)
const data = lz.compressToBase64(raw)
const data = lz.compressToBase64(text)
const body = JSON.stringify({ data, type, version, show_preview })
let id = ShareCache.get(body)
if (!id) {
@@ -16,7 +16,7 @@ export async function shareSnippet(type: string, version: VersionId, jsonData: a
ShareCache.set(body, snippet.id)
id = snippet.id as string
}
return { id, length: raw.length, compressed: data.length, rate: raw.length / data.length }
return { id, length: text.length, compressed: data.length, rate: text.length / data.length }
} catch (e) {
if (e instanceof Error) {
e.message = `Error creating share link: ${e.message}`
@@ -30,7 +30,7 @@ export async function getSnippet(id: string) {
const snippet = await fetchApi(`/${id}`)
return {
...snippet,
data: JSON.parse(lz.decompressFromBase64(snippet.data) ?? '{}'),
data: safeJsonParse(lz.decompressFromBase64(snippet.data) ?? '{}') ?? {},
}
} catch (e) {
if (e instanceof Error) {
+3 -3
View File
@@ -1,7 +1,7 @@
import { NbtTag } from 'deepslate'
import yaml from 'js-yaml'
import { Store } from '../Store.js'
import { jsonToNbt } from '../Utils.js'
import { jsonToNbt, safeJsonParse } from '../Utils.js'
const INDENTS: Record<string, number | string | undefined> = {
'2_spaces': 2,
@@ -21,7 +21,7 @@ const FORMATS: Record<string, {
snbt: {
parse: (s) => JSON.stringify(NbtTag.fromString(s).toSimplifiedJson(), null, 2),
stringify: (s, i) => {
const tag = jsonToNbt(JSON.parse(s))
const tag = jsonToNbt(safeJsonParse(s) ?? {})
if (i === undefined) {
return tag.toString()
}
@@ -30,7 +30,7 @@ const FORMATS: Record<string, {
},
yaml: {
parse: (s) => JSON.stringify(yaml.load(s), null, 2),
stringify: (s, i) => yaml.dump(JSON.parse(s), {
stringify: (s, i) => yaml.dump(safeJsonParse(s) ?? {}, {
flowLevel: i === undefined ? 0 : -1,
indent: typeof i === 'string' ? 4 : i,
}),