Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues and v2 work updates #189

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"format:prettier": "prettier --write .",
"format:eslint": "eslint --fix .",
"start": "vite build --watch",
"test": "[ -f ./dist/index.js ] || npm run build && vitest",
"test": "[ -f ./dist/index.js ] || npm run test:build",
"test:build": "npm run build && vitest",
Comment on lines +57 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't doing quite what you expect - previously it was running the build if it hadn't run already - we actually don't need that conditional run, so can just have vitest on its own - I was mixing up vite preview and vitest!

Suggested change
"test": "[ -f ./dist/index.js ] || npm run test:build",
"test:build": "npm run build && vitest",
"start:test": "vitest",
"test": "vitest run",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run build is required to capture changes to the codebase because tests run specifically against the files in /dist, and without it the test system will only reflect changes to the tests themselves. test:build forces a build of the codebase to ensure all tests run successfully against the latest code.

if there is another way to capture code changes in list I would love to use it because this honestly isn't great.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - it still needs that ternary for the moment then - I'm about to be pushing a test update that tests the built cjs (currently) via the new commandline tool and a bash script (the intention is that other implementations can support the same arguments and then run the same tests) - currently I can't get nodejs to load binary data properly though, so raw and uint8array both fail to decode :-(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the test code has been updated and fixed, so it works properly - the test strategy will be changing slightly for the production code so it'll only run when it can (and force it on github itself) - so I'll clean up this when I get to it - can undo the change in the meantime :-)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can now be removed - the test stategy has been updated (including better tests for built code, and an external test tool here - I'll get it running in github actions soon!)

"test:coverage": "vitest run src.main.test.ts --coverage",
"lint": "npm run lint:prettier && npm run lint:eslint",
"lint:prettier": "prettier --check .",
Expand Down
100 changes: 71 additions & 29 deletions src/__tests__/testFunctions.ts
Rycochet marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,39 @@ export function getTestData(name: string) {
return cachedTestData[name] || (cachedTestData[name] = readFileSync(`testdata/${name}/data.bin`).toString());
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function runGeneralTests(identifier: string, compressFunc: (input: any) => any, decompressFunc: (input: any) => any) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typedocs to be added, and use generics rather than any - but abstraction is good :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason for the any is because there are 2 incompatible function signatures, and I haven't been able to find a way to reconcile them. do you have any suggestions?

examples:
compress: (input: string | null) => T | null and (input: string) => T
decompress: (input: T | null) => string | null and (input: T) => string

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure it's possible, but might be worth merging this in and fixing later

for (const path in testDataFiles) {
const name = testDataFiles[path];

describe(name, () => {
const rawData = getTestData(path);
const compressedData = compressFunc(rawData);

test("consistent", ({ expect }) => {
expect(compressedData).toEqual(compressFunc(rawData));
});
test("alter data", ({ expect }) => {
expect(compressedData).not.toEqual(rawData);
});
test("decompresses", ({ expect }) => {
expect(decompressFunc(compressedData)).toEqual(rawData);
});

if (identifier) {
const knownCompressed = readFileSync(`testdata/${path}/js/${identifier}.bin`).toString();

test("expected compression result", ({ expect }) => {
expect(compressFunc(rawData)).toEqual(knownCompressed);
});
test(`expected decompression result`, ({ expect }) => {
expect(decompressFunc(knownCompressed)).toEqual(rawData);
});
}
});
}
}

/**
* This will run a series of tests against each compress / decompress pair.
*
Expand All @@ -59,7 +92,7 @@ export function runTestSet<T extends { length: number }>(
const compressedNull = compressFunc(null);

compressedNull instanceof Uint8Array
? expect(compressedNull.length).toBe(0)
? expect(compressedNull.byteLength).toBe(0)
: expect(compressedNull).toEqual("");
});

Expand All @@ -68,7 +101,7 @@ export function runTestSet<T extends { length: number }>(
const compressedUndefined = compressFunc(undefined!);

compressedUndefined instanceof Uint8Array
? expect(compressedUndefined.length).toBe(0)
? expect(compressedUndefined.byteLength).toBe(0)
: expect(compressedUndefined).toBe("");
});

Expand All @@ -79,41 +112,50 @@ export function runTestSet<T extends { length: number }>(
expect(compressedEmpty).toEqual(compressFunc(""));
expect(compressedEmpty).not.toEqual("");
compressedEmpty instanceof Uint8Array
? expect(compressedEmpty.length).not.toBe(0)
? expect(compressedEmpty.byteLength).not.toBe(0)
: expect(typeof compressedEmpty).toBe("string");
expect(decompressFunc(compressedEmpty)).toEqual("");
});

for (const path in testDataFiles) {
const name = testDataFiles[path];
runGeneralTests(identifier, compressFunc, decompressFunc);
}

describe(name, () => {
const rawData = getTestData(path);
const compressedData = compressFunc(rawData);
/**
* This will run a series of tests against each compress / decompress pair.
*
* All tests must (where possible):
* - Check that it doesn't output null unless expected
* - Check that the compression is deterministic
* - Check that it changes the input string
* - Check that it can decompress again
* - Check against a known good value
*/
export function runNewerTestSet<T extends { length: number }>(
identifier: string,
compressFunc: (input: string) => T,
decompressFunc: (input: T) => string,
) {
// Specific internal behaviour
test(`undefined`, ({ expect }) => {
const compressedUndefined = compressFunc(undefined!);

test("consistent", ({ expect }) => {
expect(compressedData).toEqual(compressFunc(rawData));
});
test("alter data", ({ expect }) => {
expect(compressedData).not.toEqual(rawData);
});
test("decompresses", ({ expect }) => {
expect(decompressFunc(compressedData)).toEqual(rawData);
});
compressedUndefined instanceof Uint8Array
? expect(compressedUndefined.byteLength).toBe(0)
: expect(compressedUndefined).toBe("");
});

if (identifier) {
const knownCompressed = readFileSync(`testdata/${path}/js/${identifier}.bin`).toString();
// Specific internal behaviour
test(`"" returns (empty string)`, ({ expect }) => {
const compressedEmpty = compressFunc("");

test("expected compression result", ({ expect }) => {
expect(compressFunc(rawData)).toEqual(knownCompressed);
});
test(`expected decompression result`, ({ expect }) => {
// @ts-expect-error We don't know the type
expect(decompressFunc(knownCompressed)).toEqual(rawData);
});
}
});
}
compressedEmpty instanceof Uint8Array
? expect(compressedEmpty.byteLength).not.toBe(0)
: expect(compressedEmpty).toBe("");

expect(decompressFunc(compressedEmpty)).toEqual("");
});

runGeneralTests(identifier, compressFunc, decompressFunc);
}

export async function testMockedLZString(importPath: string, displayName: string) {
Expand Down
8 changes: 6 additions & 2 deletions src/base64/base64.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
*/

import { describe } from "vitest";
import { compressToBase64, decompressFromBase64 } from ".";
import { runTestSet } from "../__tests__/testFunctions";
import { compressToBase64, compressToBase64_fixed } from ".";
import { decompressFromBase64, decompressFromBase64_fixed } from ".";
Comment on lines +8 to +9
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should merge into one import (format might want to play though!)

Suggested change
import { compressToBase64, compressToBase64_fixed } from ".";
import { decompressFromBase64, decompressFromBase64_fixed } from ".";
import {
compressToBase64,
compressToBase64_fixed,
decompressFromBase64,
decompressFromBase64_fixed,
} from ".";

import { runNewerTestSet, runTestSet } from "../__tests__/testFunctions";

describe("base64", () => {
runTestSet<string>("base64", compressToBase64, decompressFromBase64);
});
describe("betterbase64", () => {
runNewerTestSet<string>("betterbase64", compressToBase64_fixed, decompressFromBase64_fixed);
});
2 changes: 2 additions & 0 deletions src/base64/compressToBase64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import { _compress } from "../_compress";
import keyStrBase64 from "./keyStrBase64";
import { deprecated } from "../utils/misc";

export function compressToBase64(input: string | null): string {
deprecated("compressToBase64()", "v2.0.0", { replacement: "compressToBetterBase64()" });
if (input == null) {
return "";
}
Expand Down
28 changes: 28 additions & 0 deletions src/base64/compressToBase64_fixed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { _compress } from "../_compress";
import keyStrBase64 from "./keyStrBase64";

export function compressToBase64_fixed(input: string): string {
if (!input) {
return "";
}
const res = _compress(input, 6, (a) => keyStrBase64.charAt(a));

// To produce valid Base64
switch (res.length % 3) {
case 0:
return res;
case 1:
return res + "==";
case 2:
return res + "=";
default: // When could this happen ?
console.warn("Something in compressToBetterBase64() is very very wrong.");
return "";
}
}
2 changes: 2 additions & 0 deletions src/base64/decompressFromBase64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import { _decompress } from "../_decompress";
import { getBaseValue } from "../getBaseValue";
import keyStrBase64 from "./keyStrBase64";
import { deprecated } from "../utils/misc";

export function decompressFromBase64(input: string | null) {
deprecated("decompressFromBase64()", "v2.0.0", { replacement: "decompressFromBetterBase64()" });
if (input == null) return "";
if (input == "") return null;

Expand Down
17 changes: 17 additions & 0 deletions src/base64/decompressFromBase64_fixed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { _decompress } from "../_decompress";
import { getBaseValue } from "../getBaseValue";
import keyStrBase64 from "./keyStrBase64";

export function decompressFromBase64_fixed(input: string): string {
if (!input) {
return "";
}
const res = _decompress(input.length, 32, (index) => getBaseValue(keyStrBase64, input.charAt(index)));
return res || "";
}
2 changes: 2 additions & 0 deletions src/base64/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
*/

export { compressToBase64 } from "./compressToBase64";
export { compressToBase64_fixed } from "./compressToBase64_fixed";
export { decompressFromBase64 } from "./decompressFromBase64";
export { decompressFromBase64_fixed } from "./decompressFromBase64_fixed";
14 changes: 14 additions & 0 deletions src/base64URL/base64.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { describe } from "vitest";
import { compressToBase64URL } from ".";
import { decompressFromBase64URL } from ".";
import { runNewerTestSet } from "../__tests__/testFunctions";

describe("base64url", () => {
runNewerTestSet<string>("base64url", compressToBase64URL, decompressFromBase64URL);
});
15 changes: 15 additions & 0 deletions src/base64URL/compressToBase64URL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { _compress } from "../_compress";
import { keyStrBase64URL } from "./keyStrBase64URL";

export function compressToBase64URL(input: string): string {
if (!input) {
return "";
}
return _compress(input, 6, (a) => keyStrBase64URL.charAt(a));
}
17 changes: 17 additions & 0 deletions src/base64URL/decompressFromBase64URL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

import { _decompress } from "../_decompress";
import { getBaseValue } from "../getBaseValue";
import { keyStrBase64URL } from "./keyStrBase64URL";

export function decompressFromBase64URL(input: string): string {
if (!input) {
return "";
}
const res = _decompress(input.length, 32, (index) => getBaseValue(keyStrBase64URL, input.charAt(index)));
return res || "";
}
8 changes: 8 additions & 0 deletions src/base64URL/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

export { compressToBase64URL } from "./compressToBase64URL";
export { decompressFromBase64URL } from "./decompressFromBase64URL";
7 changes: 7 additions & 0 deletions src/base64URL/keyStrBase64URL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2013 Pieroxy <pieroxy@pieroxy.net>
*
* SPDX-License-Identifier: MIT
*/

export const keyStrBase64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
2 changes: 2 additions & 0 deletions src/encodedURIComponent/compressToEncodedURIComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import { _compress } from "../_compress";
import keyStrUriSafe from "./keyStrUriSafe";
import { deprecated } from "../utils/misc";

export function compressToEncodedURIComponent(input: string | null) {
deprecated("compressToEncodedURIComponent()", "v2.0.0", { replacement: "compressToBase64URL()" });
if (input == null) return "";

return _compress(input, 6, (a) => keyStrUriSafe.charAt(a));
Expand Down
2 changes: 2 additions & 0 deletions src/encodedURIComponent/decompressFromEncodedURIComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import keyStrUriSafe from "./keyStrUriSafe";
import { _decompress } from "../_decompress";
import { getBaseValue } from "../getBaseValue";
import { deprecated } from "../utils/misc";

export function decompressFromEncodedURIComponent(input: string | null) {
deprecated("decompressFromEncodedURIComponent()", "v2.0.0", { replacement: "decompressFromBase64URL()" });
if (input == null) return "";
if (input == "") return null;

Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import { _compress } from "./_compress";
import { _decompress } from "./_decompress";
import { compressToBase64, decompressFromBase64 } from "./base64";
import { compressToBase64, decompressFromBase64, compressToBase64_fixed, decompressFromBase64_fixed } from "./base64";
import { compressToBase64URL, decompressFromBase64URL } from "./base64URL";
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "./encodedURIComponent";
import { compress, decompress } from "./stock";
import { compressToUint8Array, decompressFromUint8Array } from "./Uint8Array";
Expand All @@ -22,6 +23,11 @@ export default {

compressToBase64,
decompressFromBase64,
compressToBase64_fixed,
decompressFromBase64_fixed,

compressToBase64URL,
decompressFromBase64URL,

compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
Expand Down
9 changes: 9 additions & 0 deletions src/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type VERSIONS = "v2.0.0";

export function deprecated(thing: string, version: VERSIONS, opts?: { replacement?: string }): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache whether the notice has been written for this function before, otherwise it can spam the console.

I'd actually suggest making this into a decorator instead which makes the code "cleaner" - it also means you can replace the function call with the original after the first call so no need to cache it (hmu if you need to!)

I've opened a poll on #190 which will decide whether to do this style, or fix them and provide legacy endpoints (I'm leaning towards this personally, and it's the more "lz-string" way, but if we get a strong opinion another way I'll happily go with it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you could pm or email me that would be great, I'm not actually sure what you are suggesting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started to write a reply and go through the various ways it could be done (as we can't use a real typescript / javascript decorator outside of a class) - and realised that it's premature optimisation and we can worry about this later - plus it's far more useful for developers than for the end-user, so we've got until the final release of v2 to improve matters, with this as a good place to start from :-)

let notice = `LZString | ${thing} is deprecated as of: ${version}`;
if (opts?.replacement) {
notice += ` - Please use ${opts.replacement} instead`;
}
console.error(notice);
}
1 change: 1 addition & 0 deletions testdata/all_ascii/js/base64url.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AQQgRAxAJApAZAcgBQEoBUBqANAWgHQD0ADAIwBMAzACwCsAbAOwAcAnAFwDcAPALwB8AfgACAQQBCAYQAiAUQBiAcQASASQBSAaQAyAWQByAeQAKARQBKAZQAqAVQBqAdQAaATQBaAbQA6AXQB6APoABgCGAEYAxgAmAKYAZgDmABYAlgBWANYANgC2AHYA9gAOAI4ATgDOAC4ArgBuAO4AHgCeAF4A3gA-AL4AfkA
1 change: 1 addition & 0 deletions testdata/all_ascii/js/betterbase64.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AQQgRAxAJApAZAcgBQEoBUBqANAWgHQD0ADAIwBMAzACwCsAbAOwAcAnAFwDcAPALwB8AfgACAQQBCAYQAiAUQBiAcQASASQBSAaQAyAWQByAeQAKARQBKAZQAqAVQBqAdQAaATQBaAbQA6AXQB6APoABgCGAEYAxgAmAKYAZgDmABYAlgBWANYANgC2AHYA9gAOAI4ATgDOAC4ArgBuAO4AHgCeAF4A3gA+AL4AfkA==
1 change: 1 addition & 0 deletions testdata/all_utf16/js/base64url.bin

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions testdata/all_utf16/js/betterbase64.bin

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions testdata/hello_world/js/base64url.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BIUwNmD2AEDukCcwBMg
1 change: 1 addition & 0 deletions testdata/hello_world/js/betterbase64.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BIUwNmD2AEDukCcwBMg==
1 change: 1 addition & 0 deletions testdata/lorem_ipsum/js/base64url.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DIewTgpgtgBAlgBwM4FdYBMQBtwyXAFxgEMoICAaGAYxADskJqDyUwT1E4lq46BzGBCyEqjdDExC4KJFBASWUBLj69O6FHSIoiWYgCNwEIUUw5IMKMX51iJEQEcUxAHQwAItlwI2smHT0QnRwsAhgxBCM2kJY0BDaaDAAZsS8IvhI7gCiIbB0TJIocAFa1FbEBATcwaEwujAsWFj-wvGJUDn8UQTESPDaEN3sQ-QOcM5jIQYAFjDhkdFEBGDc1ZPuALI2dqZwpI3QKuzgvEIoMDhGYNX9bWQdAXCzh82y7gCCRMQozLhsuzoZXg6ByRHkcSQ1RMaRSEBQ_H2RHElyYBH8ADces8UC1OjBtlUagk6g0mi07nEHujYEgbIRbjAMYRIqZUSB3AB5MBndDEBA4_q4lZwahRAbwNK4mpAujlcTuACSyCS9mIPIu1DYqH6zN6JloAhQJmsbBqYBm9C1_UiBWIdCZwk-ZGWwgpNAiNs4Ap4fEEJNg8MabB9Qm6UL680g1TQ7gAQs8EppA3kUmk4BkavwIsy-QEIJMaPQMcRmjV7NRPXgjhAwO4AHKFTQlWXlaxEu6pslu1pUhI0vD0jvuABqcCxYAieFkCCT3EY8xALG0Ldx-lgsII4C37D1rN8YH8qV-grBFUhLjzqTINvK6LAodGEf61iYCUjCBm6vIU8NJbLWQwPWwKGvwxpPPgTIsiYOq8AKrpvC-PyrDakj7PiHxOC4sANMcRI7uyJBoQcSDYCIvDopwDpKMc7gAMqECQLpMtgugIJURYMAWLjgshMplK4ABQgkACooes4HRGirREAU6J3LxF7EFepDipuIbcFyPIlEGVzbsSfaPNMczku8MAAGI_LwBgyVY2DYjC5TJPCiIcSicTMJi2I2XiFTDjA2RQiQgwSsZFRmv0WpgDqfmMhCDnuMA6YhGqRDOOa3D-CiGK4r4vQsDAChwCA9REAGMAGPodCcAQ7gAEqWnK_i2hA9qOlgsSMTwbQoYWpoofMbr9lEGwwIqyi1pwYzNiQ1BalAdIOvg3zMUCzT2Kk6TcCU8hgAYcANmu9gqKgtYwWRoqECgVHjM4BzCqsYr9HSiIdoV2lEd1cS9cah1yrdxpEPtBhJkk5WbRm239NmxC5vYBSFn-pYZDkQVHVAtAPrWYZTEdaZbZB5DoZ8WFTM8cwLFE_aNOJEzgdYtj2MtTFjAeZkeOhMB3fkeN4dUBHWEgdIgvmhalnTBxYlCOK-bt-2juOtZTqcJQvQyxIXBirEEOxRBFRyBLgPtFVVTVNDav4KHNToWAik9VglDR_ihLOYBTbJR3uBz91EFiBR2Iypn9HwVisFkwl1Zl_RreuONEM5GRx6U_16TcNTtoy4vczkIjImK33cH1fH9LOzTDUgha-FgzJ2OwpdxIMFfgV--29DEcuO7To0AKq2494q1iAwcOlFMX3qGgt0l0YxZ4DTwvJTSw02sdMQFpZxq29QZay0Oscfr-OQ5kWyO0czsTW7cC4-t55RJex1DY3hbCIQSoOs5YDUmDyIXRR10h7CFW8wvyMEQjjZ8zoxhBw9JGZSXBfQCFqIGC449iThl6IBAACkPY0lhSJlj_jdWe90-6inFJvRkQCmb51rIXX6MA6q6EPLAdKTcTCBGoj2foVDyhWQIj8MCJpi5Kgvu7CoQt7DVDlHATQMRWzzFxDXdUg4BQJDPDuAihpGB3SIDIxcNx4bk0dP7SoNRKx9HqlHLmxRdTqhkP0bm1i6EmD0ZwUgYJOY0VwEA3SIBriMnuP2JIYVoHxWlmvGA3IzjUJ6s4hRh5no1hGN0MwNiSAIXdJUdwABheggjwoDQtFaZqCRWoOixB1F-tUYC9ztlfF8JRWEwBmEmSAqwzDeHYAzXYowypQg8QcDh1iDj6GtNOJAs5qrzhMMeay7xhJ0X5FfB0YwRBgX0PUdaGNwCu1vqgZSx0wAgD4F7eghQHpkP6FI9QWhvgagCEEUifJISi0kgoUsUQclVg_l_WAB8gzOQREiKMRNglGMQKgWAWjuIcRCEgLAWla4mEccyCcysPoUI1ixXeutCqcANtsN8dghSkN4OKKAOkUkFJqJ-b8Kx7BjAufbBG4FwBIvUduXA0KdEizwoYl4ft3yMnMYkiQCdNI1LALsZskVizIzMXKgCrwKQ5LyeBIM3KeKzThInTABRyhjH1u4AAithS4UpnozjnELGZVlBQDBYG7JIJ1cEmAsNAM8clbLWCUipG8iVpJCilfYGVnF_yJyRgBL2nNaWgPdFLaoPkkjutpBACQVsSV1M-Z4M5BrUxuNgDlLAeVKgmFwl-aMEQTVpI8vJUoN8OI7zYhxAtpVjFColWOfUMAABWsgtxJ26RtWsvzJRakTkGIZBbRza1xQfCGmYkIRTwK_IC3AOoVJBTGWAtb_COJkQGzyJcjkhzCnCjqhpOCjr0YKgO5ZOJXqCYW6Ch0b713LoWTgth5x1HKszVSvtSl3qDbsUerQUlCUEl8bFzazCcwaLe0xVyuEDGnqk8sGTrb1GDQ-0IT7Yo0sfiwNhnxSltVpZW-wQ7tUIFGbWRt3lcTJsDdSwCDEVpjAzjUItJaCrlRjm4IC3ETB7hME7BxZr31P3AvqgI7igKPPeS8ll51nnigaETEiq66LQDZBorl9BtFar0Xy-ecxEO3E-L7OxXkOoUZ_PYTdqDo4CRgJgk9S002XETEcpB1YxV2pslcjSw8tgfP2X6tSd4QvgIwQ60YdYApUvFXcKl8jU5xUqJnLCBxZmniS9NNJHFoFZJgB8OaaBFqhXXRBVWNCfomCDEy8UomSC_B3PRXTUDOXsE1RxEz25-XmaA0hmBgFtjLoATl1NsBkirAEBmDZDQO6NDUDI25nwx3ShtGVCIRApPEbFt6bgvABBKhVBgTpK6ONEGhcwVgiXshUv6jSitDm2TNejsJ96SKSBkboO4ESPWoLdrZv0PLQWfvfnGZMzgNr_O1fNdaaNBxY0oYTTLZNxhU2gmErU_udwjnD3NtFfwznxHC2uetmIM0Ghsu_J8O8wPWtg7TCeSH9O8FWqmfDlEcKDowDotzuHC4VDLmqPW2Om5mcvpgCOWdHFXximJTBveQNTaMWIYGao8nsiKTvgctMN5tXk6fBg9w3ccOsze_S4BdKpyT0E7kri937yXGx5IXNFOmZLISGzuZCk7KQmhLNT4x2EGCBFNLQsGb8xORcsCk5htg9X3SbEdoA4AXpkXQjg-K2mm0CgPITA09nxbpkBuf7dvKMVEZqHtd8L4nOwdBCRO8iUR6MYBfGAGawtby7JhykGekiYt1NBNkcQSoypR5d2TcCfSncj13cCsfZOAtcro9-I6n3uHMtvx4M0IfN6h5ATr3fY8hxadVNpjEYZwx70wpIrDCxDOgSrE-hxlDn1dmIgTez76DRlzSaNa5xJx34yKCamqDK8zbj8y4CO4iwqYAySyMayxGwlCVT2g1SM7zB0Z7aOjSxJo7osYvaZoE5CBE4ShgbBYPgSoiRrayLoZtqc5uqBruCYJEYjTNyGAsjtzoHLzR6SSlJHpsheoKSQZ66EShoNCtqOJ_58yEDwF9BTwwAiQxYIFU6MFFAlB07Bqn4BQXAZYGTD75BGKhL2ThLJzUDsGcFsLESFoK63YgBF4KAlQKgwDZJVgogpZJwVKrqSH6bsAcSx6opKyCYcyeSFo2b9ChocR3YsBu4pqe6yYIF0iqIA6C5eYCLgSjKYiKyTiObj6gTqooCl5xZNIQoj7C7TKbJ2zZo6awB8AsDDAg6sh9pQglS9K167A_L4b7DjoSp76fz4aH6BbH4sHDLPpor2Dv5jQOj2gFQuj-CcCRFp5eK9bOHF4GzKiQqe4WDXYsxOEu4JFsAcDwKL6dToZXLL4RLsaHGXDtZeQmKMi7pISMwNhhxsg-oG6Ra3hfKRiaG3Lu7-I1Dr7AqoqkBYbZGNZUrqhahoSREW6krkJDiUIfQxIFx2Gs5d67JdGxHVKmrEiphGHPT1ZxK9EH7FD-72ohwTHZwN4dSwRcDfDh4XHAygyBhoykr1LxbDCQYNG8nYytbtEDq9Jn67JNKvGS6pSq66ydYSDlQNBd5QTTEAxjBikwDAAQAlQAhUYWEh4cSBKPDwhhbLqF5bFOLByiJXy8QSKrbSJaHyJVxKLsBpHLKmBWZMjRHtTV7vZOYxathlGRhm6RhzZ-iLbHQgIoZH5sZkl2EhwrEDhDLxEPZET0bVKSFiFfHhYAHXjZqEn9BSl_6NqOH2FtoWYSqSHfERYbSqS3jBi0GpY9CRjuGYQSyFqoFJAraYFTK3YWz9Cx4cSfabaXp4aPCtZul-4HZcERJtmOKfaDivRonRJ4BxmVx-CJIXyWIxTmmuEqlhEVBEqwIVYLT2h_TyheY9lmwiAgy-ZNpq42FAGHaSSa7MQKEERinCQjjen4mcSMCu6nFJF6qFCpG-7vxjGAT1hGIioKLVx8DKJcYub_TuHZCGQDjiaxYtleYoi96C6vnqlBS0ZpDpkSghmE79bWnkAW6AbPE1DQIhxLG6i5S6ClrnCrmxJ2E-EuyTQ2nna7E6iw41ELngF5iJlJDJCyBihtpEVij4GzEfCsl-h-ZBim7oIfjRgV7sF4EMaEFMbEEiGkG1HkGDzE7UFvxmasYk4xSIVB4JRzFwjDGPAupnTYaAiFCAkxCLzUw4nYxgmlabCnxf5Wk_42nSn_6G7Tl2FVJxyQYfD3JDJPLhavKqZJUaY660j4XVLbC-q1nG7qSNmYWiqf6-AgYMr7Y25VoMn2k3IxBdECb5D0CJR-L6RD6johIoZhIh6BljRSgToXBToxpRlgIY5EHu6QD4iWQEw7T8EYX9HbauX2AHoBRoVJBNEJZ-ZIH3m4qbgMEbYWQQVfH6mp6wh-XIheZSkjV6W74HXk4hxBgUoVSJjVTBLfZkWfD3IHxKm6YCaA7A56KzyrIsbhDHIOhnrrrrwlCwh8K4DQmWWw0qavobIlk4otrwY0UdrIZgIMWfE8YsUsCJQWr7kFGtEGhqqNaaiGYwrfDx4owWT74DijHs7jF6G3HVHw53VBQLEuKg1GKGXUEEYvhHWznMRbVxGU08qGXyLFHsLbQk2fDtYw0Ij0zFxtb5JIEiWLVb6OU0hCRAA
Loading