mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries
- The Electron mirror of the feature switch is awaited; a mirror that cannot be written undoes the settings write, so a reload never starts from a mirror that disagrees with the persisted switch. - A failed multi-type re-stamp rolls the store back AND re-stamps every touched type from it, since earlier types may already carry the new locks; a failed rollback keeps the playlist stale (fail-closed). - A persisted lock store whose nested entries are not what writeLocks produces is a failed read, not an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
b64defb576
commit
559ec5508c
10 files changed
+193
-23
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
isParentalLockPlaylistLocksEmpty,
|
||||
isWellFormedParentalLockStore,
|
||||
lockedStalkerCategoryIds,
|
||||
lockedXtreamCategoryIds,
|
||||
normalizeParentalLockPlaylistLocks,
|
||||
@@ -109,3 +110,40 @@ describe('parental-lock.util', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWellFormedParentalLockStore', () => {
|
||||
it('accepts what writeLocks produces, lists omitted or empty', () => {
|
||||
expect(isWellFormedParentalLockStore({})).toBe(true);
|
||||
expect(
|
||||
isWellFormedParentalLockStore({
|
||||
p: {
|
||||
xtream: [{ categoryType: 'live', xtreamId: 7 }],
|
||||
stalker: [{ categoryType: 'itv', categoryId: '9' }],
|
||||
m3u: ['Adult'],
|
||||
},
|
||||
q: { m3u: [] },
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a non-object root', []],
|
||||
['a non-object playlist entry', { p: 'corrupt' }],
|
||||
['a non-list lock field', { p: { xtream: 'corrupt' } }],
|
||||
['a non-string group title', { p: { m3u: [1] } }],
|
||||
[
|
||||
'an unknown category type',
|
||||
{ p: { xtream: [{ categoryType: 'x', xtreamId: 1 }] } },
|
||||
],
|
||||
[
|
||||
'a non-numeric xtream id',
|
||||
{ p: { xtream: [{ categoryType: 'live', xtreamId: 'nope' }] } },
|
||||
],
|
||||
[
|
||||
'the Stalker "all" pseudo id',
|
||||
{ p: { stalker: [{ categoryType: 'itv', categoryId: '*' }] } },
|
||||
],
|
||||
])('rejects %s', (_, value) => {
|
||||
expect(isWellFormedParentalLockStore(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -152,6 +152,57 @@ export function normalizeParentalLockStalkerCategories(
|
||||
return result;
|
||||
}
|
||||
|
||||
function isWellFormedList(
|
||||
value: unknown,
|
||||
isWellFormedEntry: (entry: unknown) => boolean
|
||||
): boolean {
|
||||
return (
|
||||
value === undefined ||
|
||||
(Array.isArray(value) && value.every(isWellFormedEntry))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a persisted store has the shape `writeLocks` produces: every
|
||||
* playlist entry an object whose optional `xtream`/`stalker`/`m3u` lists
|
||||
* hold only entries the normalizers accept. The normalizers DROP what they
|
||||
* do not understand, which is right for user-supplied backups but turns a
|
||||
* corrupted persisted store into "nothing is locked"; a store that fails
|
||||
* this check is treated as unreadable instead.
|
||||
*/
|
||||
export function isWellFormedParentalLockStore(value: unknown): boolean {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(value).every(
|
||||
(locks) =>
|
||||
isRecord(locks) &&
|
||||
isWellFormedList(
|
||||
locks['xtream'],
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_XTREAM_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockXtreamCategoryType
|
||||
) &&
|
||||
normalizeNumericId(entry['xtreamId']) !== null
|
||||
) &&
|
||||
isWellFormedList(
|
||||
locks['stalker'],
|
||||
(entry) =>
|
||||
isRecord(entry) &&
|
||||
PARENTAL_LOCK_STALKER_CATEGORY_TYPES.includes(
|
||||
entry['categoryType'] as ParentalLockStalkerCategoryType
|
||||
) &&
|
||||
((typeof entry['categoryId'] === 'string' &&
|
||||
entry['categoryId'].trim() !== '' &&
|
||||
entry['categoryId'].trim() !== '*') ||
|
||||
(typeof entry['categoryId'] === 'number' &&
|
||||
Number.isFinite(entry['categoryId'])))
|
||||
) &&
|
||||
isWellFormedList(locks['m3u'], (entry) => typeof entry === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeParentalLockGroupTitles(value: unknown): string[] {
|
||||
const seen = new Set<string>();
|
||||
for (const item of toArray(value)) {
|
||||
|
||||
Reference in new issue
Block a user