このコミットが含まれているのは:
2026-07-18 13:07:49 +09:00
コミット 06b9c1cb50
2個のファイルの変更126行の追加24行の削除
+36 -12
ファイルの表示
@@ -205,7 +205,10 @@ const isValidSkipReason = (
value === 'existing' || value === 'manual' value === 'existing' || value === 'manual'
const isPositiveInteger = (value: unknown): value is number => const isPositiveInteger = (value: unknown): value is number =>
Number.isInteger (value) && Number (value) > 0 Number.isSafeInteger (value) && Number (value) > 0
const isNonEmptyMessageRecord = (value: Record<string, string[]>): boolean =>
Object.values (value).some (messages => messages.length > 0)
const hasOnlyKeys = ( const hasOnlyKeys = (
value: Record<string, unknown>, value: Record<string, unknown>,
@@ -285,6 +288,13 @@ const sanitiseExistingPost = (value: unknown): PostImportExistingPost | undefine
thumbnailUrl: value.thumbnailUrl as string | undefined } thumbnailUrl: value.thumbnailUrl as string | undefined }
} }
const canonicalisePersistedRow = (
row: PostImportRow,
): PostImportRow | null => {
const sanitised = sanitiseRow (serialisePostImportRow (row))
return sanitised == null ? null : recalculateThumbnailWarning (sanitised)
}
const recalculateThumbnailWarning = (row: PostImportRow): PostImportRow => { const recalculateThumbnailWarning = (row: PostImportRow): PostImportRow => {
const others = (row.fieldWarnings.thumbnailBase ?? []).filter ( const others = (row.fieldWarnings.thumbnailBase ?? []).filter (
key => key !== 'サムネールなし') key => key !== 'サムネールなし')
@@ -379,9 +389,15 @@ export const serialisePostImportRows = (rows: PostImportRow[]) =>
export const serialisedPostImportRowsEqual = ( export const serialisedPostImportRowsEqual = (
left: PostImportRow[], left: PostImportRow[],
right: PostImportRow[], right: PostImportRow[],
): boolean => ): boolean => {
JSON.stringify (serialisePostImportRows (left)) const canonicalLeft = left.map (canonicalisePersistedRow)
=== JSON.stringify (serialisePostImportRows (right)) const canonicalRight = right.map (canonicalisePersistedRow)
if (canonicalLeft.some (row => row == null) || canonicalRight.some (row => row == null))
return false
return JSON.stringify (serialisePostImportRows (canonicalLeft as PostImportRow[]))
=== JSON.stringify (serialisePostImportRows (canonicalRight as PostImportRow[]))
}
const sanitiseRow = (value: unknown): PostImportRow | null => { const sanitiseRow = (value: unknown): PostImportRow | null => {
@@ -409,8 +425,6 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
return null return null
if (value.skipReason === 'existing' && !(isPositiveInteger (value.existingPostId))) if (value.skipReason === 'existing' && !(isPositiveInteger (value.existingPostId)))
return null return null
if (value.skipReason !== 'existing' && value.existingPostId != null)
return null
if (value.importStatus === 'created' && !(isPositiveInteger (value.createdPostId))) if (value.importStatus === 'created' && !(isPositiveInteger (value.createdPostId)))
return null return null
if (value.importStatus !== 'created' && value.createdPostId != null) if (value.importStatus !== 'created' && value.createdPostId != null)
@@ -462,21 +476,31 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
return null return null
} }
const existingPost = sanitiseExistingPost (value.existingPost)
const existingPostId =
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined
if (existingPost != null && existingPostId == null)
return null
if (existingPost != null && existingPost.id !== existingPostId)
return null
return { return {
sourceRow: Number (value.sourceRow), sourceRow: Number (value.sourceRow),
url: value.url, url: value.url,
attributes: value.attributes as Record<string, string | number>, attributes: value.attributes as Record<string, string | number>,
fieldWarnings, fieldWarnings: isNonEmptyMessageRecord (fieldWarnings) ? fieldWarnings : { },
baseWarnings: value.baseWarnings, baseWarnings: value.baseWarnings,
validationErrors, validationErrors: isNonEmptyMessageRecord (validationErrors) ? validationErrors : { },
importErrors, importErrors:
importErrors != null && isNonEmptyMessageRecord (importErrors)
? importErrors
: undefined,
provenance: value.provenance as Record<string, PostImportOrigin>, provenance: value.provenance as Record<string, PostImportOrigin>,
tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined, tagSources: value.tagSources as Record<PostImportOrigin, string> | undefined,
status: value.status, status: value.status,
skipReason: value.skipReason ?? undefined, skipReason: value.skipReason ?? undefined,
existingPostId: existingPostId,
isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined, existingPost,
existingPost: sanitiseExistingPost (value.existingPost),
metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined, metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined,
resetSnapshot, resetSnapshot,
createdPostId: createdPostId:
+90 -12
ファイルの表示
@@ -101,6 +101,7 @@ const ENCODED_ROW_KEYS = [
'i', 'i',
'c', 'c',
'r'] as const 'r'] as const
const ENCODED_STATE_KEYS = ['v', 'rows'] as const
const ALLOWED_MANUAL_FIELDS = [ const ALLOWED_MANUAL_FIELDS = [
'title', 'title',
'thumbnailBase', 'thumbnailBase',
@@ -200,12 +201,19 @@ const decodeImportStatus = (
} }
const isStrictPositiveIntegerString = (value: string): boolean =>
/^[1-9][0-9]*$/.test (value)
&& Number.isSafeInteger (Number (value))
const parsePositiveInt = (value: string | null): number | null => { const parsePositiveInt = (value: string | null): number | null => {
if (value == null || value === '') if (value == null || value === '')
return null return null
const parsed = Number.parseInt (value, 10) if (!(isStrictPositiveIntegerString (value)))
return Number.isInteger (parsed) && parsed > 0 ? parsed : null return null
return Number (value)
} }
@@ -244,9 +252,37 @@ const compressBytes = async (value: Uint8Array): Promise<Uint8Array> => {
const decompressBytes = async (value: Uint8Array): Promise<Uint8Array> => { const decompressBytes = async (value: Uint8Array): Promise<Uint8Array> => {
const stream = const stream =
new Blob ([value]).stream ().pipeThrough (new DecompressionStream ('gzip')) new Blob ([value]).stream ().pipeThrough (new DecompressionStream ('gzip'))
const bytes = new Uint8Array (await new Response (stream).arrayBuffer ()) const reader = stream.getReader ()
if (bytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES) const chunks: Uint8Array[] = []
throw new Error ('decompressed state too large') let totalBytes = 0
try
{
while (true)
{
const result = await reader.read ()
if (result.done)
break
totalBytes += result.value.byteLength
if (totalBytes > MAX_DECOMPRESSED_STATE_BYTES)
{
await reader.cancel ()
throw new Error ('decompressed state too large')
}
chunks.push (result.value)
}
}
finally
{
reader.releaseLock ()
}
const bytes = new Uint8Array (totalBytes)
let offset = 0
chunks.forEach (chunk => {
bytes.set (chunk, offset)
offset += chunk.byteLength
})
return bytes return bytes
} }
@@ -408,21 +444,31 @@ const parseEncodedRow = (
return null return null
if (existingPostId != null if (existingPostId != null
&& !(typeof existingPostId === 'number' && !(typeof existingPostId === 'number'
&& Number.isInteger (existingPostId) && Number.isSafeInteger (existingPostId)
&& existingPostId > 0)) && existingPostId > 0))
return null return null
if (createdPostId != null if (createdPostId != null
&& !(typeof createdPostId === 'number' && !(typeof createdPostId === 'number'
&& Number.isInteger (createdPostId) && Number.isSafeInteger (createdPostId)
&& createdPostId > 0)) && createdPostId > 0))
return null return null
if (recoverable != null && typeof recoverable !== 'boolean') if (recoverable != null && typeof recoverable !== 'boolean')
return null return null
if (sourceRow != null if (sourceRow != null
&& !(typeof sourceRow === 'number' && !(typeof sourceRow === 'number'
&& Number.isInteger (sourceRow) && Number.isSafeInteger (sourceRow)
&& sourceRow > 0)) && sourceRow > 0))
return null return null
if (skip === 'existing' && existingPostId == null)
return null
if (importStatus === 'created' && createdPostId == null)
return null
if (importStatus !== 'created' && createdPostId != null)
return null
if (recoverable === true
&& importStatus !== 'failed'
&& importStatus !== 'pending')
return null
const stringFields = [ const stringFields = [
['t', 'title'], ['t', 'title'],
@@ -515,16 +561,41 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
const skipValue = params.get ('skip') const skipValue = params.get ('skip')
const importStatus = params.get ('import_status') const importStatus = params.get ('import_status')
const recoverableValue = params.get ('recoverable')
const recoverable = const recoverable =
params.get ('recoverable') == null recoverableValue == null
? null ? null
: params.get ('recoverable') === 'true' : recoverableValue === 'true'
? true
: recoverableValue === 'false'
? false
: null
if (skipValue != null && !(isValidSkipReason (skipValue))) if (skipValue != null && !(isValidSkipReason (skipValue)))
return null return null
if (importStatus != null && !(isValidImportStatus (importStatus))) if (importStatus != null && !(isValidImportStatus (importStatus)))
return null return null
if (recoverableValue != null && recoverable == null)
return null
if (!(hasOnlyAllowedManualFields (parseManual (params.get ('manual'))))) if (!(hasOnlyAllowedManualFields (parseManual (params.get ('manual')))))
return null return null
const existingPostIdValue = params.get ('existing_post_id')
const createdPostIdValue = params.get ('created_post_id')
const existingPostId = parsePositiveInt (existingPostIdValue)
const createdPostId = parsePositiveInt (createdPostIdValue)
if (existingPostIdValue != null && existingPostId == null)
return null
if (createdPostIdValue != null && createdPostId == null)
return null
if (skipValue === 'existing' && existingPostId == null)
return null
if (importStatus === 'created' && createdPostId == null)
return null
if (importStatus !== 'created' && createdPostIdValue != null)
return null
if (recoverable === true
&& importStatus !== 'failed'
&& importStatus !== 'pending')
return null
const row = buildRow ({ const row = buildRow ({
source_row: 1, source_row: 1,
@@ -539,9 +610,9 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
parent_post_ids: params.get ('parent_post_ids') ?? '', parent_post_ids: params.get ('parent_post_ids') ?? '',
manual: parseManual (params.get ('manual')), manual: parseManual (params.get ('manual')),
skip: skipValue, skip: skipValue,
existing_post_id: parsePositiveInt (params.get ('existing_post_id')), existing_post_id: existingPostId,
import_status: importStatus, import_status: importStatus,
created_post_id: parsePositiveInt (params.get ('created_post_id')), created_post_id: createdPostId,
recoverable }) recoverable })
return { return {
@@ -559,6 +630,8 @@ const parseMetaRows = (
const decodedBytes = decodeBase64UrlBytes (metaValue) const decodedBytes = decodeBase64UrlBytes (metaValue)
if (decodedBytes == null) if (decodedBytes == null)
return null return null
if (decodedBytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES)
return null
let parsed: MultiRowMetaState let parsed: MultiRowMetaState
try try
@@ -618,6 +691,8 @@ const parseFullState = async (stateValue: string): Promise<ParsedPostNewState |
} }
if (parsed.v !== 1 if (parsed.v !== 1
|| !(isPlainObject (parsed))
|| !(hasOnlyKeys (parsed as Record<string, unknown>, ENCODED_STATE_KEYS))
|| !(Array.isArray (parsed.rows)) || !(Array.isArray (parsed.rows))
|| parsed.rows.length === 0 || parsed.rows.length === 0
|| parsed.rows.length > MAX_STATE_ROWS) || parsed.rows.length > MAX_STATE_ROWS)
@@ -754,6 +829,9 @@ export const appendPostNewSessionId = (
export const parsePostNewState = async ( export const parsePostNewState = async (
search: string, search: string,
): Promise<ParsedPostNewState | null> => { ): Promise<ParsedPostNewState | null> => {
if (byteLength (search) > MAX_QUERY_STRING_BYTES)
return null
const params = new URLSearchParams (search) const params = new URLSearchParams (search)
const stateValue = params.get ('state') const stateValue = params.get ('state')
if (stateValue != null) if (stateValue != null)