mirror of
https://git.unlock-music.dev/um/um-react.git
synced 2024-11-27 17:42:16 +00:00
Merge pull request '酷我 iOS 数据库支持' (#60) from feat/kwm-ios-support into main
Reviewed-on: um/um-react#60
This commit is contained in:
commit
da853ba6e0
@ -7,9 +7,11 @@ export interface KuwoHeader {
|
|||||||
quality: string;
|
quality: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KUWO_MAGIC_HDRS = new Set(['yeelion-kuwo\x00\x00\x00\x00', 'yeelion-kuwo-tme']);
|
||||||
|
|
||||||
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
|
export function parseKuwoHeader(view: DataView): KuwoHeader | null {
|
||||||
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
|
const magic = view.buffer.slice(view.byteOffset, view.byteOffset + 0x10);
|
||||||
if (bytesToUTF8String(magic) !== 'yeelion-kuwo-tme') {
|
if (!KUWO_MAGIC_HDRS.has(bytesToUTF8String(magic))) {
|
||||||
return null; // not kuwo-encrypted file
|
return null; // not kuwo-encrypted file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { objectify } from 'radash';
|
|||||||
|
|
||||||
export function productionKeyToStaging<S, P extends Record<string, unknown>>(
|
export function productionKeyToStaging<S, P extends Record<string, unknown>>(
|
||||||
src: P,
|
src: P,
|
||||||
make: (k: keyof P, v: P[keyof P]) => null | S
|
make: (k: keyof P, v: P[keyof P]) => null | S,
|
||||||
): S[] {
|
): S[] {
|
||||||
const result: S[] = [];
|
const result: S[] = [];
|
||||||
for (const [key, value] of Object.entries(src)) {
|
for (const [key, value] of Object.entries(src)) {
|
||||||
@ -31,7 +31,7 @@ export const qmc2StagingToProductionKey = (key: StagingQMCv2Key) => key.name.nor
|
|||||||
export const qmc2StagingToProductionValue = (key: StagingQMCv2Key) => key.ekey.trim();
|
export const qmc2StagingToProductionValue = (key: StagingQMCv2Key) => key.ekey.trim();
|
||||||
export const qmc2ProductionToStaging = (
|
export const qmc2ProductionToStaging = (
|
||||||
key: keyof ProductionQMCv2Keys,
|
key: keyof ProductionQMCv2Keys,
|
||||||
value: ProductionQMCv2Keys[keyof ProductionQMCv2Keys]
|
value: ProductionQMCv2Keys[keyof ProductionQMCv2Keys],
|
||||||
): StagingQMCv2Key => {
|
): StagingQMCv2Key => {
|
||||||
return {
|
return {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@ -44,7 +44,13 @@ export const qmc2ProductionToStaging = (
|
|||||||
|
|
||||||
export interface StagingKWMv2Key {
|
export interface StagingKWMv2Key {
|
||||||
id: string;
|
id: string;
|
||||||
|
/**
|
||||||
|
* Resource ID
|
||||||
|
*/
|
||||||
rid: string;
|
rid: string;
|
||||||
|
/**
|
||||||
|
* Quality String
|
||||||
|
*/
|
||||||
quality: string;
|
quality: string;
|
||||||
ekey: string;
|
ekey: string;
|
||||||
}
|
}
|
||||||
@ -58,16 +64,17 @@ export const parseKwm2ProductionKey = (key: string): null | { rid: string; quali
|
|||||||
|
|
||||||
return { rid, quality };
|
return { rid, quality };
|
||||||
};
|
};
|
||||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality}`;
|
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality.replace(/[\D]/g, '')}`;
|
||||||
export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey;
|
export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey;
|
||||||
export const kwm2ProductionToStaging = (
|
export const kwm2ProductionToStaging = (
|
||||||
key: keyof ProductionKWMv2Keys,
|
key: keyof ProductionKWMv2Keys,
|
||||||
value: ProductionKWMv2Keys[keyof ProductionKWMv2Keys]
|
value: ProductionKWMv2Keys[keyof ProductionKWMv2Keys],
|
||||||
): null | StagingKWMv2Key => {
|
): null | StagingKWMv2Key => {
|
||||||
if (typeof value !== 'string') return null;
|
if (typeof value !== 'string') return null;
|
||||||
|
|
||||||
const parsed = parseKwm2ProductionKey(key);
|
const parsed = parseKwm2ProductionKey(key);
|
||||||
if (!parsed) return null;
|
if (!parsed) return null;
|
||||||
|
const { quality, rid } = parsed;
|
||||||
|
|
||||||
return { id: nanoid(), rid: parsed.rid, quality: parsed.quality, ekey: value };
|
return { id: nanoid(), rid, quality, ekey: value };
|
||||||
};
|
};
|
||||||
|
33
src/features/settings/panels/KWMv2/InstructionsIOS.tsx
Normal file
33
src/features/settings/panels/KWMv2/InstructionsIOS.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv';
|
||||||
|
|
||||||
|
export function InstructionsIOS() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text>你需要越狱来访问 iOS 应用的私有数据。</Text>
|
||||||
|
<Text>
|
||||||
|
⚠️ 请注意,越狱通常意味着你的设备
|
||||||
|
<chakra.span color="red.400">将失去保修资格</chakra.span>。
|
||||||
|
</Text>
|
||||||
|
<OrderedList>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
访问设备的这个目录:
|
||||||
|
<Code wordBreak="break-word">{KUWO_IOS_DIR}</Code>
|
||||||
|
</Text>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
提取密钥数据库文件 <Code>kw_ekey</Code> 至浏览器可访问的目录,如下载目录。
|
||||||
|
</Text>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
提交刚刚提取的 <Code>kw_ekey</Code> 密钥数据库。
|
||||||
|
</Text>
|
||||||
|
</ListItem>
|
||||||
|
</OrderedList>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
||||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
||||||
import { InstructionsPC } from './InstructionsPC';
|
import { InstructionsPC } from './InstructionsPC';
|
||||||
|
import { InstructionsIOS } from './InstructionsIOS';
|
||||||
|
|
||||||
export function KWMv2AllInstructions() {
|
export function KWMv2AllInstructions() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>安卓</Tab>
|
<Tab>安卓</Tab>
|
||||||
|
<Tab>iOS</Tab>
|
||||||
<Tab>Windows</Tab>
|
<Tab>Windows</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels flex={1} overflow="auto">
|
<TabPanels flex={1} overflow="auto">
|
||||||
@ -16,6 +18,9 @@ export function KWMv2AllInstructions() {
|
|||||||
file="cn.kuwo.player.mmkv.defaultconfig"
|
file="cn.kuwo.player.mmkv.defaultconfig"
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<InstructionsIOS />
|
||||||
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<InstructionsPC />
|
<InstructionsPC />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -22,7 +22,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
|
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
|
||||||
|
|
||||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||||
import { MMKVParser } from '~/util/MMKVParser';
|
import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo';
|
||||||
|
|
||||||
import { kwm2AddKey, kwm2ClearKeys, kwm2ImportKeys } from '../settingsSlice';
|
import { kwm2AddKey, kwm2ClearKeys, kwm2ImportKeys } from '../settingsSlice';
|
||||||
import { selectStagingKWMv2Keys } from '../settingsSelector';
|
import { selectStagingKWMv2Keys } from '../settingsSelector';
|
||||||
@ -41,9 +41,11 @@ export function PanelKWMv2Key() {
|
|||||||
const handleSecretImport = async (file: File) => {
|
const handleSecretImport = async (file: File) => {
|
||||||
let keys: Omit<StagingKWMv2Key, 'id'>[] | null = null;
|
let keys: Omit<StagingKWMv2Key, 'id'>[] | null = null;
|
||||||
if (/cn\.kuwo\.player\.mmkv/i.test(file.name)) {
|
if (/cn\.kuwo\.player\.mmkv/i.test(file.name)) {
|
||||||
const fileBuffer = await file.arrayBuffer();
|
keys = parseAndroidKuwoEKey(new DataView(await file.arrayBuffer()));
|
||||||
keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer));
|
} else if (/kw_ekey/.test(file.name)) {
|
||||||
|
keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys?.length === 0) {
|
if (keys?.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
title: '未导入密钥',
|
title: '未导入密钥',
|
||||||
|
@ -29,7 +29,7 @@ import { InfoOutlineIcon } from '@chakra-ui/icons';
|
|||||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||||
import { StagingQMCv2Key } from '../keyFormats';
|
import { StagingQMCv2Key } from '../keyFormats';
|
||||||
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
|
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
|
||||||
import { MMKVParser } from '~/util/MMKVParser';
|
import { parseAndroidQmEKey } from '~/util/mmkv/qm';
|
||||||
import { getFileName } from '~/util/pathHelper';
|
import { getFileName } from '~/util/pathHelper';
|
||||||
import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions';
|
import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions';
|
||||||
import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions';
|
import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions';
|
||||||
@ -63,7 +63,7 @@ export function PanelQMCv2Key() {
|
|||||||
}
|
}
|
||||||
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
|
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
|
||||||
const fileBuffer = await file.arrayBuffer();
|
const fileBuffer = await file.arrayBuffer();
|
||||||
const map = MMKVParser.toStringMap(new DataView(fileBuffer));
|
const map = parseAndroidQmEKey(new DataView(fileBuffer));
|
||||||
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,11 @@ export const theme = extendTheme({
|
|||||||
'Segoe UI,Helvetica,Arial,sans-serif',
|
'Segoe UI,Helvetica,Arial,sans-serif',
|
||||||
'Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol',
|
'Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol',
|
||||||
].join(','),
|
].join(','),
|
||||||
|
mono: [
|
||||||
|
'SFMono-Regular,Menlo,Monaco',
|
||||||
|
'"Sarasa Mono CJK SC",',
|
||||||
|
'Consolas,"Liberation Mono","Courier New",monospace',
|
||||||
|
].join(','),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Button: {
|
Button: {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
|
||||||
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
import { bytesToUTF8String } from '~/decrypt-worker/util/utf8Encoder';
|
||||||
import { formatHex } from './formatHex';
|
import { formatHex } from './formatHex';
|
||||||
|
|
||||||
@ -69,7 +68,11 @@ export class MMKVParser {
|
|||||||
return bytesToUTF8String(data).normalize();
|
return bytesToUTF8String(data).normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public readOptionalString() {
|
public readKey() {
|
||||||
|
return this.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public readStringValue(): string | null {
|
||||||
// Container [
|
// Container [
|
||||||
// len: int,
|
// len: int,
|
||||||
// data: variant
|
// data: variant
|
||||||
@ -96,37 +99,4 @@ export class MMKVParser {
|
|||||||
const containerLen = this.readInt();
|
const containerLen = this.readInt();
|
||||||
this.offset += containerLen;
|
this.offset += containerLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toStringMap(view: DataView): Map<string, string> {
|
|
||||||
const mmkv = new MMKVParser(view);
|
|
||||||
const result = new Map<string, string>();
|
|
||||||
while (!mmkv.eof) {
|
|
||||||
const key = mmkv.readString();
|
|
||||||
const value = mmkv.readOptionalString();
|
|
||||||
if (value) {
|
|
||||||
result.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static parseKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] {
|
|
||||||
const mmkv = new MMKVParser(view);
|
|
||||||
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
|
||||||
while (!mmkv.eof) {
|
|
||||||
const key = mmkv.readString();
|
|
||||||
const idMatch = key.match(/^sec_ekey#(\d+)-(.+)/);
|
|
||||||
if (!idMatch) {
|
|
||||||
mmkv.skipContainer();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [_, rid, quality] = idMatch;
|
|
||||||
const ekey = mmkv.readOptionalString();
|
|
||||||
if (ekey) {
|
|
||||||
result.push({ rid, quality, ekey });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,28 @@
|
|||||||
import { MMKVParser } from '../MMKVParser';
|
import { MMKVParser } from '../MMKVParser';
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
|
|
||||||
const makeViewFromBuffer = (buff: Buffer) =>
|
const makeViewFromBuffer = (buff: Buffer) =>
|
||||||
new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength));
|
new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength));
|
||||||
|
|
||||||
test('parse qm mmkv file', () => {
|
|
||||||
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm.mmkv'));
|
|
||||||
expect(Object.fromEntries(MMKVParser.toStringMap(view).entries())).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"Lorem Ipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum congue volutpat metus non molestie. Quisque id est sapien. Fusce eget tristique sem. Donec tellus lacus, viverra sed lectus eget, elementum ultrices dolor. Integer non urna justo.",
|
|
||||||
"key": "value",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parse qm mmkv file with optional str', () => {
|
|
||||||
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm_optional.mmkv'));
|
|
||||||
expect(Object.fromEntries(MMKVParser.toStringMap(view).entries())).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"key": "value",
|
|
||||||
"key2": "value2",
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parse kuwo mmkv file', () => {
|
|
||||||
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo.mmkv'));
|
|
||||||
expect(MMKVParser.parseKuwoEKey(view)).toMatchInlineSnapshot(`
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"ekey": "xyz123",
|
|
||||||
"quality": "20201kmflac",
|
|
||||||
"rid": "1234567",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throw error on broken file', () => {
|
test('throw error on broken file', () => {
|
||||||
const view = makeViewFromBuffer(
|
const view = makeViewFromBuffer(
|
||||||
Buffer.from([0x27, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x6b, 0x65, 0x79, 0x06, 0x07, 0x62, 0x61, 0x64, 0xff, 0xff]),
|
Buffer.from([0x27, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x6b, 0x65, 0x79, 0x06, 0x07, 0x62, 0x61, 0x64, 0xff, 0xff]),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(() => Object.fromEntries(MMKVParser.toStringMap(view).entries())).toThrow(/offset mismatch/i);
|
expect(() => {
|
||||||
|
const parser = new MMKVParser(view);
|
||||||
|
parser.readKey();
|
||||||
|
parser.readStringValue();
|
||||||
|
}).toThrow(/offset mismatch/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('able to handle empty value', () => {
|
||||||
|
const view = makeViewFromBuffer(
|
||||||
|
Buffer.from([0x0b, 0x00, 0x00, 0x00, 0x7f, 0x03, 0x6b, 0x65, 0x79, 0x00, 0x01, 0x31, 0x02, 0x01, 0x32, 0xff]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parser = new MMKVParser(view);
|
||||||
|
expect(parser.readKey()).toEqual('key');
|
||||||
|
expect(parser.readStringValue()).toEqual(null);
|
||||||
|
expect(parser.readKey()).toEqual('1');
|
||||||
|
expect(parser.readStringValue()).toEqual('2');
|
||||||
});
|
});
|
||||||
|
Binary file not shown.
BIN
src/util/mmkv/__tests__/__fixture__/kuwo_ios.mmkv
Normal file
BIN
src/util/mmkv/__tests__/__fixture__/kuwo_ios.mmkv
Normal file
Binary file not shown.
Binary file not shown.
31
src/util/mmkv/__tests__/kuwo.test.ts
Normal file
31
src/util/mmkv/__tests__/kuwo.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '../kuwo';
|
||||||
|
|
||||||
|
const makeViewFromBuffer = (buff: Buffer) =>
|
||||||
|
new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength));
|
||||||
|
|
||||||
|
test('parse kuwo android ekey mmkv file "cn.kuwo.player.mmkv.defaultconfig"', () => {
|
||||||
|
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo_android.mmkv'));
|
||||||
|
expect(parseAndroidKuwoEKey(view)).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ekey": "xyz123",
|
||||||
|
"quality": "20201kmflac",
|
||||||
|
"rid": "1234567",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parse kuwo ios ekey mmkv file "kw_ekey"', () => {
|
||||||
|
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/kuwo_ios.mmkv'));
|
||||||
|
expect(parseIosKuwoEKey(view)).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ekey": "xyz123",
|
||||||
|
"quality": "20201",
|
||||||
|
"rid": "1234567",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
15
src/util/mmkv/__tests__/qm.test.ts
Normal file
15
src/util/mmkv/__tests__/qm.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { parseAndroidQmEKey } from '../qm';
|
||||||
|
|
||||||
|
const makeViewFromBuffer = (buff: Buffer) =>
|
||||||
|
new DataView(buff.buffer.slice(buff.byteOffset, buff.byteOffset + buff.byteLength));
|
||||||
|
|
||||||
|
test('parse qm mmkv file', () => {
|
||||||
|
const view = makeViewFromBuffer(readFileSync(__dirname + '/__fixture__/qm.mmkv'));
|
||||||
|
expect(Object.fromEntries(parseAndroidQmEKey(view).entries())).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"Lorem Ipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum congue volutpat metus non molestie. Quisque id est sapien. Fusce eget tristique sem. Donec tellus lacus, viverra sed lectus eget, elementum ultrices dolor. Integer non urna justo.",
|
||||||
|
"key": "value",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
41
src/util/mmkv/kuwo.ts
Normal file
41
src/util/mmkv/kuwo.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import type { StagingKWMv2Key } from '~/features/settings/keyFormats';
|
||||||
|
import { MMKVParser } from '../MMKVParser';
|
||||||
|
|
||||||
|
export function parseAndroidKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] {
|
||||||
|
const mmkv = new MMKVParser(view);
|
||||||
|
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
||||||
|
while (!mmkv.eof) {
|
||||||
|
const key = mmkv.readString();
|
||||||
|
const idMatch = key.match(/^sec_ekey#(\d+)-(\w+)$/);
|
||||||
|
if (!idMatch) {
|
||||||
|
mmkv.skipContainer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, rid, quality] = idMatch;
|
||||||
|
const ekey = mmkv.readStringValue();
|
||||||
|
if (ekey) {
|
||||||
|
result.push({ rid, quality, ekey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIosKuwoEKey(view: DataView): Omit<StagingKWMv2Key, 'id'>[] {
|
||||||
|
const mmkv = new MMKVParser(view);
|
||||||
|
const result: Omit<StagingKWMv2Key, 'id'>[] = [];
|
||||||
|
while (!mmkv.eof) {
|
||||||
|
const key = mmkv.readKey();
|
||||||
|
const idMatch = key.match(/^(\d+)_(\d+)$/);
|
||||||
|
if (!idMatch) {
|
||||||
|
mmkv.skipContainer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [_, rid, quality] = idMatch;
|
||||||
|
const ekey = mmkv.readStringValue();
|
||||||
|
if (ekey) {
|
||||||
|
result.push({ rid, quality, ekey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
14
src/util/mmkv/qm.ts
Normal file
14
src/util/mmkv/qm.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MMKVParser } from '../MMKVParser';
|
||||||
|
|
||||||
|
export function parseAndroidQmEKey(view: DataView): Map<string, string> {
|
||||||
|
const mmkv = new MMKVParser(view);
|
||||||
|
const result = new Map<string, string>();
|
||||||
|
while (!mmkv.eof) {
|
||||||
|
const key = mmkv.readString();
|
||||||
|
const value = mmkv.readStringValue();
|
||||||
|
if (value) {
|
||||||
|
result.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
@ -114,7 +114,6 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
// workaround: sql.js is not ESModule friendly, yet...
|
// workaround: sql.js is not ESModule friendly, yet...
|
||||||
deps: {
|
deps: {
|
||||||
// inline: ['sql.js'],
|
|
||||||
optimizer: {
|
optimizer: {
|
||||||
web: {
|
web: {
|
||||||
include: ['sql.js'],
|
include: ['sql.js'],
|
||||||
|
Loading…
Reference in New Issue
Block a user