fix(macos): support verified QQ builds on arm64 and x64
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { join } = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const nativePath = join(__dirname, '../../../packages/napcat-native');
|
||||
const napiPath = join(nativePath, 'napi2native', 'napi2native.darwin.' + process.arch + '.node');
|
||||
const packetPath = join(nativePath, 'packet', 'MoeHoo.darwin.' + process.arch + '.node');
|
||||
|
||||
async function probe (mode) {
|
||||
if (mode === 'missing-wrapper') {
|
||||
const napi = require(napiPath);
|
||||
assert.throws(() => napi.initHook('1', '2'), /Failed to initialize send hook/);
|
||||
const packet = require(packetPath);
|
||||
assert.equal(packet.initHook('2', '4', () => {}, false), 'error search');
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = require('./wrapper.node');
|
||||
const addresses = wrapper.addresses();
|
||||
const napi = require(napiPath);
|
||||
assert.equal(napi.initHook(addresses.coerce, addresses.resolver), true);
|
||||
const lengths = [0, 1, 21, 22, 23, 24, 255, 4096];
|
||||
for (const length of lengths) {
|
||||
const payload = Buffer.from(Array.from({ length }, (_, index) => index % 256));
|
||||
assert.deepEqual(wrapper.convert(payload), payload, 'convert length ' + length);
|
||||
const error = Buffer.from('e'.repeat(length));
|
||||
const result = await wrapper.resolve(-17, error, payload);
|
||||
assert.equal(result.result, -17);
|
||||
assert.equal(result.errMsg, error.toString());
|
||||
assert.equal(result.rsp, payload.toString());
|
||||
assert.deepEqual(result.rspbuffer, payload, 'resolve length ' + length);
|
||||
}
|
||||
for (const value of ['', '文🙂'.repeat(20), 12345, null, true]) {
|
||||
assert.deepEqual(wrapper.convert(value), Buffer.from(String(value)));
|
||||
}
|
||||
const responses = await Promise.all(Array.from({ length: 64 }, (_, index) => {
|
||||
const payload = Buffer.from([0, index, 255, 128]);
|
||||
return wrapper.resolve(index, Buffer.alloc(0), payload).then(result => {
|
||||
assert.equal(result.result, index);
|
||||
assert.deepEqual(result.rspbuffer, payload);
|
||||
});
|
||||
}));
|
||||
assert.equal(responses.length, 64);
|
||||
|
||||
const packet = require(packetPath);
|
||||
const received = [];
|
||||
assert.equal(packet.initHook(addresses.send, addresses.recv, (...args) => received.push(args), false), 'success');
|
||||
assert.equal(wrapper.emptyPackets(), 7);
|
||||
const expected = [];
|
||||
for (const length of lengths) {
|
||||
const uin = Buffer.from('1'.repeat(length));
|
||||
const command = Buffer.from('C'.repeat(length));
|
||||
const payload = Buffer.from(Array.from({ length }, (_, index) => index % 256));
|
||||
for (const direction of [0, 1]) {
|
||||
const sequence = length + 3000;
|
||||
const result = wrapper.packet(direction, uin, command, sequence, payload);
|
||||
assert.equal(result.returned, direction === 0 ? 102 : 209);
|
||||
assert.deepEqual(result.command, command);
|
||||
expected.push([direction, direction === 0 ? '0' : uin.toString(), command.toString(), sequence, payload.toString('hex').toUpperCase()]);
|
||||
}
|
||||
}
|
||||
const embeddedNull = Buffer.from('A\0B');
|
||||
wrapper.packet(1, embeddedNull, embeddedNull, 99, Buffer.from([255, 0, 128]));
|
||||
expected.push([1, 'A\0B', 'A\0B', 99, 'FF0080']);
|
||||
packet.setO3Setting(true);
|
||||
const replaced = wrapper.packet(0, Buffer.from('1'), Buffer.from('trpc.o3.report.Report.SsoReport'), 0, Buffer.alloc(0));
|
||||
assert.equal(replaced.command.toString(), 'fuck.trpc.o3');
|
||||
packet.setO3Setting(false);
|
||||
for (let attempt = 0; received.length < expected.length && attempt < 100; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
assert.deepEqual(received, expected);
|
||||
}
|
||||
|
||||
if (process.argv[2] === '--probe') {
|
||||
probe(process.argv[3]).catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
} else {
|
||||
for (const mode of ['missing-wrapper', 'hooks']) {
|
||||
test('macOS ' + process.arch + ' native ' + mode, () => {
|
||||
const result = spawnSync(process.execPath, [__filename, '--probe', mode], {
|
||||
encoding: 'utf8',
|
||||
timeout: 20000,
|
||||
});
|
||||
assert.ifError(result.error);
|
||||
assert.equal(result.signal, null, result.stderr);
|
||||
assert.equal(result.status, 0, result.stderr + result.stdout);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
#include <node_api.h>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct alignas(8) QQString
|
||||
{
|
||||
std::array<uint8_t, 24> bytes{};
|
||||
};
|
||||
|
||||
#if defined(__aarch64__)
|
||||
constexpr size_t flagOffset = 23;
|
||||
constexpr size_t dataOffset = 0;
|
||||
constexpr uint8_t heapMask = 0x80;
|
||||
#else
|
||||
constexpr size_t flagOffset = 0;
|
||||
constexpr size_t dataOffset = 16;
|
||||
constexpr uint8_t heapMask = 1;
|
||||
#endif
|
||||
|
||||
static uint64_t ReadWord(const QQString &value, size_t offset)
|
||||
{
|
||||
uint64_t word;
|
||||
std::memcpy(&word, value.bytes.data() + offset, sizeof(word));
|
||||
return word;
|
||||
}
|
||||
|
||||
static void WriteWord(QQString &value, size_t offset, uint64_t word)
|
||||
{
|
||||
std::memcpy(value.bytes.data() + offset, &word, sizeof(word));
|
||||
}
|
||||
|
||||
static QQString MakeString(const void *data, size_t length)
|
||||
{
|
||||
QQString value;
|
||||
if (length <= 22)
|
||||
{
|
||||
#if defined(__aarch64__)
|
||||
std::memcpy(value.bytes.data(), data, length);
|
||||
value.bytes[23] = length;
|
||||
#else
|
||||
std::memcpy(value.bytes.data() + 1, data, length);
|
||||
value.bytes[0] = length * 2;
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t capacity = (length | 15) + 1;
|
||||
auto buffer = static_cast<uint8_t *>(std::calloc(capacity, 1));
|
||||
std::memcpy(buffer, data, length);
|
||||
WriteWord(value, dataOffset, reinterpret_cast<uintptr_t>(buffer));
|
||||
WriteWord(value, 8, length);
|
||||
#if defined(__aarch64__)
|
||||
WriteWord(value, 16, capacity | (uint64_t{1} << 63));
|
||||
#else
|
||||
WriteWord(value, 0, capacity | 1);
|
||||
#endif
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string ReadString(const QQString &value)
|
||||
{
|
||||
if ((value.bytes[flagOffset] & heapMask) != 0)
|
||||
return {reinterpret_cast<const char *>(ReadWord(value, dataOffset)), ReadWord(value, 8)};
|
||||
#if defined(__aarch64__)
|
||||
return {reinterpret_cast<const char *>(value.bytes.data()), value.bytes[23]};
|
||||
#else
|
||||
return {reinterpret_cast<const char *>(value.bytes.data() + 1), static_cast<size_t>(value.bytes[0] / 2)};
|
||||
#endif
|
||||
}
|
||||
|
||||
static void FreeString(const QQString &value)
|
||||
{
|
||||
if ((value.bytes[flagOffset] & heapMask) != 0)
|
||||
std::free(reinterpret_cast<void *>(ReadWord(value, dataOffset)));
|
||||
}
|
||||
|
||||
static QQString FromBuffer(napi_env env, napi_value input)
|
||||
{
|
||||
void *data;
|
||||
size_t length;
|
||||
napi_get_buffer_info(env, input, &data, &length);
|
||||
return MakeString(data, length);
|
||||
}
|
||||
|
||||
extern "C" __attribute__((noinline)) QQString MockCoerce(napi_env env, napi_value input)
|
||||
{
|
||||
napi_value text;
|
||||
napi_coerce_to_string(env, input, &text);
|
||||
size_t length;
|
||||
napi_get_value_string_utf8(env, text, nullptr, 0, &length);
|
||||
std::vector<char> bytes(length + 1);
|
||||
napi_get_value_string_utf8(env, text, bytes.data(), bytes.size(), &length);
|
||||
return MakeString(bytes.data(), length);
|
||||
}
|
||||
|
||||
struct Context
|
||||
{
|
||||
uint8_t padding[24]{};
|
||||
napi_env env;
|
||||
napi_deferred deferred;
|
||||
};
|
||||
|
||||
struct AsyncResult
|
||||
{
|
||||
uint8_t padding[16]{};
|
||||
Context *context;
|
||||
int32_t result;
|
||||
int32_t alignment{};
|
||||
QQString error;
|
||||
QQString response;
|
||||
};
|
||||
|
||||
extern "C" __attribute__((noinline)) void MockResolver(AsyncResult *result)
|
||||
{
|
||||
napi_throw_error(result->context->env, nullptr, "Resolver was not intercepted");
|
||||
}
|
||||
|
||||
static volatile int64_t sendCalls = 0;
|
||||
static volatile int64_t recvCalls = 0;
|
||||
|
||||
extern "C" __attribute__((noinline)) int64_t MockSend(void *self, void **packet)
|
||||
{
|
||||
sendCalls += 1;
|
||||
return 101 + (self != nullptr) + (packet != nullptr);
|
||||
}
|
||||
|
||||
extern "C" __attribute__((noinline)) int64_t MockRecv(void *self, void **packet, uint32_t marker)
|
||||
{
|
||||
recvCalls += 1;
|
||||
return 201 + marker + (self != nullptr) + (packet != nullptr);
|
||||
}
|
||||
|
||||
static napi_value Convert(napi_env env, napi_callback_info info)
|
||||
{
|
||||
size_t argc = 1;
|
||||
napi_value input;
|
||||
napi_get_cb_info(env, info, &argc, &input, nullptr, nullptr);
|
||||
auto output = MockCoerce(env, input);
|
||||
auto bytes = ReadString(output);
|
||||
napi_value result;
|
||||
napi_create_buffer_copy(env, bytes.size(), bytes.data(), nullptr, &result);
|
||||
FreeString(output);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value Resolve(napi_env env, napi_callback_info info)
|
||||
{
|
||||
size_t argc = 3;
|
||||
napi_value args[3];
|
||||
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
|
||||
Context context{};
|
||||
context.env = env;
|
||||
napi_value promise;
|
||||
napi_create_promise(env, &context.deferred, &promise);
|
||||
AsyncResult result{};
|
||||
result.context = &context;
|
||||
napi_get_value_int32(env, args[0], &result.result);
|
||||
result.error = FromBuffer(env, args[1]);
|
||||
result.response = FromBuffer(env, args[2]);
|
||||
MockResolver(&result);
|
||||
FreeString(result.error);
|
||||
FreeString(result.response);
|
||||
return promise;
|
||||
}
|
||||
|
||||
struct ByteVector
|
||||
{
|
||||
uint8_t *begin;
|
||||
uint8_t *end;
|
||||
};
|
||||
|
||||
struct SendInner
|
||||
{
|
||||
QQString command;
|
||||
uint64_t padding{};
|
||||
ByteVector *payload;
|
||||
};
|
||||
|
||||
struct SendPacket
|
||||
{
|
||||
SendInner *inner;
|
||||
uint8_t padding[48]{};
|
||||
QQString uin;
|
||||
uint64_t alignment{};
|
||||
int64_t sequence;
|
||||
uint8_t tail[16]{};
|
||||
};
|
||||
|
||||
struct RecvPacket
|
||||
{
|
||||
QQString uin;
|
||||
int64_t sequence;
|
||||
QQString command;
|
||||
ByteVector *payload;
|
||||
};
|
||||
|
||||
static_assert(offsetof(AsyncResult, error) == 32);
|
||||
static_assert(offsetof(AsyncResult, response) == 56);
|
||||
static_assert(offsetof(SendPacket, uin) == 56);
|
||||
static_assert(offsetof(SendPacket, sequence) == 88);
|
||||
static_assert(offsetof(RecvPacket, payload) == 56);
|
||||
|
||||
static napi_value Packet(napi_env env, napi_callback_info info)
|
||||
{
|
||||
size_t argc = 5;
|
||||
napi_value args[5];
|
||||
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
|
||||
int32_t direction;
|
||||
int64_t sequence;
|
||||
napi_get_value_int32(env, args[0], &direction);
|
||||
napi_get_value_int64(env, args[3], &sequence);
|
||||
auto uin = FromBuffer(env, args[1]);
|
||||
auto command = FromBuffer(env, args[2]);
|
||||
void *payloadData;
|
||||
size_t payloadLength;
|
||||
napi_get_buffer_info(env, args[4], &payloadData, &payloadLength);
|
||||
uint8_t emptyPayload = 0;
|
||||
auto bytes = payloadLength ? static_cast<uint8_t *>(payloadData) : &emptyPayload;
|
||||
ByteVector payload{bytes, bytes + payloadLength};
|
||||
int64_t returnValue;
|
||||
if (direction == 0)
|
||||
{
|
||||
SendInner inner{command, 0, &payload};
|
||||
SendPacket packet{};
|
||||
packet.inner = &inner;
|
||||
packet.uin = uin;
|
||||
packet.sequence = sequence;
|
||||
void *pointer = &packet;
|
||||
returnValue = MockSend(nullptr, &pointer);
|
||||
command = inner.command;
|
||||
}
|
||||
else
|
||||
{
|
||||
RecvPacket packet{uin, sequence, command, &payload};
|
||||
void *pointer = &packet;
|
||||
returnValue = MockRecv(nullptr, &pointer, 7);
|
||||
}
|
||||
napi_value result;
|
||||
napi_create_object(env, &result);
|
||||
napi_value returned;
|
||||
napi_create_int64(env, returnValue, &returned);
|
||||
napi_set_named_property(env, result, "returned", returned);
|
||||
auto commandBytes = ReadString(command);
|
||||
napi_value outputCommand;
|
||||
napi_create_buffer_copy(env, commandBytes.size(), commandBytes.data(), nullptr, &outputCommand);
|
||||
napi_set_named_property(env, result, "command", outputCommand);
|
||||
FreeString(uin);
|
||||
FreeString(command);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value EmptyPackets(napi_env env, napi_callback_info info)
|
||||
{
|
||||
void *missing = nullptr;
|
||||
MockSend(nullptr, nullptr);
|
||||
MockSend(nullptr, &missing);
|
||||
SendPacket send{};
|
||||
void *sendPointer = &send;
|
||||
MockSend(nullptr, &sendPointer);
|
||||
SendInner inner{};
|
||||
send.inner = &inner;
|
||||
MockSend(nullptr, &sendPointer);
|
||||
MockRecv(nullptr, nullptr, 0);
|
||||
MockRecv(nullptr, &missing, 0);
|
||||
RecvPacket recv{};
|
||||
void *recvPointer = &recv;
|
||||
MockRecv(nullptr, &recvPointer, 0);
|
||||
napi_value result;
|
||||
napi_create_int64(env, sendCalls + recvCalls, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value Addresses(napi_env env, napi_callback_info info)
|
||||
{
|
||||
Dl_info image{};
|
||||
dladdr(reinterpret_cast<void *>(MockCoerce), &image);
|
||||
uintptr_t base = reinterpret_cast<uintptr_t>(image.dli_fbase);
|
||||
napi_value result;
|
||||
napi_create_object(env, &result);
|
||||
const std::pair<const char *, uintptr_t> functions[] = {
|
||||
{"coerce", reinterpret_cast<uintptr_t>(MockCoerce) - base},
|
||||
{"resolver", reinterpret_cast<uintptr_t>(MockResolver) - base},
|
||||
{"send", (reinterpret_cast<uintptr_t>(MockSend) - base) * 2},
|
||||
{"recv", (reinterpret_cast<uintptr_t>(MockRecv) - base) * 2},
|
||||
};
|
||||
for (const auto &entry : functions)
|
||||
{
|
||||
std::ostringstream hex;
|
||||
hex << std::hex << entry.second;
|
||||
napi_value value;
|
||||
napi_create_string_utf8(env, hex.str().c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, result, entry.first, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports)
|
||||
{
|
||||
const napi_property_descriptor properties[] = {
|
||||
{"addresses", nullptr, Addresses, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"convert", nullptr, Convert, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"resolve", nullptr, Resolve, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"packet", nullptr, Packet, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"emptyPackets", nullptr, EmptyPackets, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
};
|
||||
napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties);
|
||||
return exports;
|
||||
}
|
||||
|
||||
NAPI_MODULE(wrapper, Init)
|
||||
@@ -0,0 +1,56 @@
|
||||
name: macOS native compatibility
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- packages/napcat-native/**
|
||||
- .github/tests/macos-native/**
|
||||
- .github/workflows/macos-native.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/napcat-native/**
|
||||
- .github/tests/macos-native/**
|
||||
- .github/workflows/macos-native.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
native:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15
|
||||
arch: arm64
|
||||
- runner: macos-15-intel
|
||||
arch: x64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
architecture: ${{ matrix.arch }}
|
||||
- name: Build QQ ABI mock
|
||||
working-directory: .github/tests/macos-native
|
||||
run: |
|
||||
npm install --prefix headers --ignore-scripts --no-package-lock node-api-headers@1.6.0
|
||||
clang++ -std=c++17 -O0 -g -bundle -undefined dynamic_lookup -I headers/node_modules/node-api-headers/include wrapper.cpp -o wrapper.node
|
||||
codesign --force --sign - wrapper.node
|
||||
- name: Exercise native packet hooks
|
||||
run: node --test .github/tests/macos-native/hooks.test.cjs
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: NapNeko/ffmpegAddon
|
||||
ref: 3a6e99cea9a856ca71f7d70e11d6d769498121d9
|
||||
path: ffmpeg-native-tests
|
||||
- name: Verify packaged FFmpeg
|
||||
working-directory: ffmpeg-native-tests
|
||||
env:
|
||||
FFMPEG_ADDON_PATH: ${{ github.workspace }}/packages/napcat-native/ffmpeg/ffmpegAddon.darwin.${{ matrix.arch }}.node
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
node --test test/video-info.test.js
|
||||
+82
-2
@@ -165,7 +165,7 @@
|
||||
},
|
||||
"6.9.88-44725-x64": {
|
||||
"send": "2756EF6",
|
||||
"recv": "0A36152"
|
||||
"recv": "A36152"
|
||||
},
|
||||
"6.9.88-44725-arm64": {
|
||||
"send": "2313C68",
|
||||
@@ -241,7 +241,7 @@
|
||||
},
|
||||
"6.9.93-47354-x64": {
|
||||
"send": "28ADE46",
|
||||
"recv": "0A8FD5A"
|
||||
"recv": "A8FD5A"
|
||||
},
|
||||
"9.9.30-48517-x64": {
|
||||
"send": "0A9B1BC",
|
||||
@@ -402,5 +402,85 @@
|
||||
"9.9.36-53644-x64": {
|
||||
"send": "C3EE2F",
|
||||
"recv": "24A6355"
|
||||
},
|
||||
"6.9.82-40768-x64": {
|
||||
"send": "6587B8",
|
||||
"recv": "C45C6E"
|
||||
},
|
||||
"6.9.82-40990-x64": {
|
||||
"send": "6587B8",
|
||||
"recv": "C45C6E"
|
||||
},
|
||||
"6.9.83-41679-x64": {
|
||||
"send": "271A206",
|
||||
"recv": "A1E04A"
|
||||
},
|
||||
"6.9.83-41785-x64": {
|
||||
"send": "2744756",
|
||||
"recv": "A1DE4A"
|
||||
},
|
||||
"6.9.83-41857-x64": {
|
||||
"send": "2745186",
|
||||
"recv": "A1E96E"
|
||||
},
|
||||
"6.9.85-42086-x64": {
|
||||
"send": "27462F6",
|
||||
"recv": "A1FC0A"
|
||||
},
|
||||
"6.9.86-42744-x64": {
|
||||
"send": "27770E6",
|
||||
"recv": "A2725A"
|
||||
},
|
||||
"6.9.86-42905-x64": {
|
||||
"send": "2786206",
|
||||
"recv": "A2D6A6"
|
||||
},
|
||||
"6.9.86-42941-x64": {
|
||||
"send": "2786366",
|
||||
"recv": "A2D2E2"
|
||||
},
|
||||
"6.9.87-44343-x64": {
|
||||
"send": "2750306",
|
||||
"recv": "A2F432"
|
||||
},
|
||||
"6.9.89-45758-x64": {
|
||||
"send": "288CBB6",
|
||||
"recv": "A87816"
|
||||
},
|
||||
"6.9.90-46494-x64": {
|
||||
"send": "28A2F96",
|
||||
"recv": "A8D2FE"
|
||||
},
|
||||
"6.9.91-46928-x64": {
|
||||
"send": "28A2F96",
|
||||
"recv": "A8D2FE"
|
||||
},
|
||||
"6.9.95-48517-x64": {
|
||||
"send": "28DC616",
|
||||
"recv": "AAED2A"
|
||||
},
|
||||
"6.9.96-49738-x64": {
|
||||
"send": "2B8C936",
|
||||
"recv": "AFF03E"
|
||||
},
|
||||
"6.9.98-51102-x64": {
|
||||
"send": "2C6CD06",
|
||||
"recv": "B42922"
|
||||
},
|
||||
"6.9.99-51802-x64": {
|
||||
"send": "2F6C1AB",
|
||||
"recv": "C16A4A"
|
||||
},
|
||||
"7.0.0-52194-x64": {
|
||||
"send": "2FAC58B",
|
||||
"recv": "C20DB2"
|
||||
},
|
||||
"7.0.1-52892-x64": {
|
||||
"send": "304CC8B",
|
||||
"recv": "C3A54E"
|
||||
},
|
||||
"7.0.2-53644-x64": {
|
||||
"send": "309B1BB",
|
||||
"recv": "C47262"
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -914,5 +914,85 @@
|
||||
"9.9.36-53644-x64": {
|
||||
"send": "38255F0",
|
||||
"recv": "3828E30"
|
||||
},
|
||||
"6.9.82-40768-x64": {
|
||||
"send": "489A168",
|
||||
"recv": "489CD18"
|
||||
},
|
||||
"6.9.82-40990-x64": {
|
||||
"send": "489A168",
|
||||
"recv": "489CD18"
|
||||
},
|
||||
"6.9.83-41679-x64": {
|
||||
"send": "450C8E0",
|
||||
"recv": "450F490"
|
||||
},
|
||||
"6.9.83-41785-x64": {
|
||||
"send": "450C540",
|
||||
"recv": "450F0F0"
|
||||
},
|
||||
"6.9.83-41857-x64": {
|
||||
"send": "450D998",
|
||||
"recv": "4510548"
|
||||
},
|
||||
"6.9.85-42086-x64": {
|
||||
"send": "450E430",
|
||||
"recv": "4510FE0"
|
||||
},
|
||||
"6.9.86-42744-x64": {
|
||||
"send": "4570018",
|
||||
"recv": "4572BC8"
|
||||
},
|
||||
"6.9.86-42905-x64": {
|
||||
"send": "457E7C0",
|
||||
"recv": "4581370"
|
||||
},
|
||||
"6.9.86-42941-x64": {
|
||||
"send": "457EA78",
|
||||
"recv": "4581628"
|
||||
},
|
||||
"6.9.87-44343-x64": {
|
||||
"send": "45126C0",
|
||||
"recv": "4515270"
|
||||
},
|
||||
"6.9.89-45758-x64": {
|
||||
"send": "478B860",
|
||||
"recv": "478E410"
|
||||
},
|
||||
"6.9.90-46494-x64": {
|
||||
"send": "47B8018",
|
||||
"recv": "47BABC8"
|
||||
},
|
||||
"6.9.91-46928-x64": {
|
||||
"send": "47B8018",
|
||||
"recv": "47BABC8"
|
||||
},
|
||||
"6.9.95-48517-x64": {
|
||||
"send": "48A92C0",
|
||||
"recv": "48ABE70"
|
||||
},
|
||||
"6.9.96-49738-x64": {
|
||||
"send": "4ACD9D8",
|
||||
"recv": "4AD0588"
|
||||
},
|
||||
"6.9.98-51102-x64": {
|
||||
"send": "4C953E8",
|
||||
"recv": "4C97F98"
|
||||
},
|
||||
"6.9.99-51802-x64": {
|
||||
"send": "5293B28",
|
||||
"recv": "52966D8"
|
||||
},
|
||||
"7.0.0-52194-x64": {
|
||||
"send": "53143C0",
|
||||
"recv": "5316F70"
|
||||
},
|
||||
"7.0.1-52892-x64": {
|
||||
"send": "54552F8",
|
||||
"recv": "5457EA8"
|
||||
},
|
||||
"7.0.2-53644-x64": {
|
||||
"send": "54F11F0",
|
||||
"recv": "54F3DA8"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user