mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge commit '5b013073bf'
# Conflicts: # CLAUDE.md # MODULE.bazel.lock # submodules/TgVoipWebrtc/tgcalls # third-party/td/build-td-bazel.sh # third-party/webrtc/webrtc
This commit is contained in:
commit
8dc06f48ce
1570 changed files with 133567 additions and 34276 deletions
|
|
@ -106,13 +106,7 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base
|
|||
destination_path = destination_base_path + '/' + file_name
|
||||
allowed_file_extensions = ['.mobileprovision', '.cer', '.p12']
|
||||
if os.path.isfile(source_path) and any(source_path.endswith(ext) for ext in allowed_file_extensions):
|
||||
#print('Decrypting {} to {} with {}'.format(source_path, destination_path, password))
|
||||
os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format(
|
||||
password=password,
|
||||
source_path=source_path,
|
||||
destination_path=destination_path
|
||||
))
|
||||
#decrypt_match_data(source_path, destination_path, password)
|
||||
decrypt_match_data(source_path, destination_path, password)
|
||||
elif os.path.isdir(source_path):
|
||||
os.makedirs(destination_path, exist_ok=True)
|
||||
decrypt_codesigning_directory_recursively(source_path, destination_path, password)
|
||||
|
|
|
|||
|
|
@ -1,221 +1,293 @@
|
|||
import os
|
||||
import base64
|
||||
import subprocess
|
||||
import tempfile
|
||||
import hashlib
|
||||
|
||||
class EncryptionV1:
|
||||
ALGORITHM = 'aes-256-cbc'
|
||||
|
||||
def decrypt(self, encrypted_data, password, salt, hash_algorithm="MD5"):
|
||||
try:
|
||||
return self._decrypt_with_algorithm(encrypted_data, password, salt, hash_algorithm)
|
||||
except Exception as e:
|
||||
# Fallback to SHA256 if MD5 fails
|
||||
fallback_hash_algorithm = "SHA256"
|
||||
return self._decrypt_with_algorithm(encrypted_data, password, salt, fallback_hash_algorithm)
|
||||
# FIPS-197 AES S-box and inverse S-box.
|
||||
_SBOX = bytes.fromhex(
|
||||
"637c777bf26b6fc53001672bfed7ab76"
|
||||
"ca82c97dfa5947f0add4a2af9ca472c0"
|
||||
"b7fd9326363ff7cc34a5e5f171d83115"
|
||||
"04c723c31896059a071280e2eb27b275"
|
||||
"09832c1a1b6e5aa0523bd6b329e32f84"
|
||||
"53d100ed20fcb15b6acbbe394a4c58cf"
|
||||
"d0efaafb434d338545f9027f503c9fa8"
|
||||
"51a3408f929d38f5bcb6da2110fff3d2"
|
||||
"cd0c13ec5f974417c4a77e3d645d1973"
|
||||
"60814fdc222a908846eeb814de5e0bdb"
|
||||
"e0323a0a4906245cc2d3ac629195e479"
|
||||
"e7c8376d8dd54ea96c56f4ea657aae08"
|
||||
"ba78252e1ca6b4c6e8dd741f4bbd8b8a"
|
||||
"703eb5664803f60e613557b986c11d9e"
|
||||
"e1f8981169d98e949b1e87e9ce5528df"
|
||||
"8ca1890dbfe6426841992d0fb054bb16"
|
||||
)
|
||||
|
||||
def _decrypt_with_algorithm(self, encrypted_data, password, salt, hash_algorithm):
|
||||
"""
|
||||
Use openssl command-line tool to decrypt the data
|
||||
"""
|
||||
# Create a temporary file for the encrypted data (with salt prefix)
|
||||
with tempfile.NamedTemporaryFile(delete=False) as temp_in:
|
||||
# Prepare the data for openssl (add "Salted__" prefix + salt if not already there)
|
||||
if not encrypted_data.startswith(b"Salted__"):
|
||||
temp_in.write(b"Salted__" + salt + encrypted_data)
|
||||
_INV_SBOX = bytes.fromhex(
|
||||
"52096ad53036a538bf40a39e81f3d7fb"
|
||||
"7ce339829b2fff87348e4344c4dee9cb"
|
||||
"547b9432a6c2233dee4c950b42fac34e"
|
||||
"082ea16628d924b2765ba2496d8bd125"
|
||||
"72f8f66486689816d4a45ccc5d65b692"
|
||||
"6c704850fdedb9da5e154657a78d9d84"
|
||||
"90d8ab008cbcd30af7e45805b8b34506"
|
||||
"d02c1e8fca3f0f02c1afbd0301138a6b"
|
||||
"3a9111414f67dcea97f2cfcef0b4e673"
|
||||
"96ac7422e7ad3585e2f937e81c75df6e"
|
||||
"47f11a711d29c5896fb7620eaa18be1b"
|
||||
"fc563e4bc6d279209adbc0fe78cd5af4"
|
||||
"1fdda8338807c731b11210592780ec5f"
|
||||
"60517fa919b54a0d2de57a9f93c99cef"
|
||||
"a0e03b4dae2af5b0c8ebbb3c83539961"
|
||||
"172b047eba77d626e169146355210c7d"
|
||||
)
|
||||
|
||||
_RCON = bytes.fromhex("01020408102040801b36")
|
||||
|
||||
|
||||
def _xtime(a):
|
||||
return (((a << 1) ^ 0x1b) & 0xff) if (a & 0x80) else (a << 1)
|
||||
|
||||
|
||||
def _gf_mul(a, b):
|
||||
r = 0
|
||||
for _ in range(8):
|
||||
if b & 1:
|
||||
r ^= a
|
||||
b >>= 1
|
||||
a = _xtime(a)
|
||||
return r
|
||||
|
||||
|
||||
def _key_expansion_256(key):
|
||||
# AES-256: Nk=8, Nr=14, total 4 * (Nr + 1) = 60 words = 240 bytes.
|
||||
if len(key) != 32:
|
||||
raise ValueError("AES-256 key must be 32 bytes")
|
||||
w = bytearray(240)
|
||||
w[:32] = key
|
||||
i = 32
|
||||
while i < 240:
|
||||
t = bytearray(w[i - 4:i])
|
||||
if i % 32 == 0:
|
||||
t = bytearray([t[1], t[2], t[3], t[0]])
|
||||
for j in range(4):
|
||||
t[j] = _SBOX[t[j]]
|
||||
t[0] ^= _RCON[i // 32 - 1]
|
||||
elif i % 32 == 16:
|
||||
for j in range(4):
|
||||
t[j] = _SBOX[t[j]]
|
||||
for j in range(4):
|
||||
w[i + j] = w[i - 32 + j] ^ t[j]
|
||||
i += 4
|
||||
return [bytes(w[r * 16:(r + 1) * 16]) for r in range(15)]
|
||||
|
||||
|
||||
def _add_round_key(state, rk):
|
||||
return bytes(s ^ k for s, k in zip(state, rk))
|
||||
|
||||
|
||||
def _sub_bytes(state):
|
||||
return bytes(_SBOX[b] for b in state)
|
||||
|
||||
|
||||
def _inv_sub_bytes(state):
|
||||
return bytes(_INV_SBOX[b] for b in state)
|
||||
|
||||
|
||||
# Column-major state: state[r + 4 * c], r = 0..3 (row), c = 0..3 (column).
|
||||
def _shift_rows(state):
|
||||
s = bytearray(state)
|
||||
s[1], s[5], s[9], s[13] = s[5], s[9], s[13], s[1]
|
||||
s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6]
|
||||
s[3], s[7], s[11], s[15] = s[15], s[3], s[7], s[11]
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def _inv_shift_rows(state):
|
||||
s = bytearray(state)
|
||||
s[1], s[5], s[9], s[13] = s[13], s[1], s[5], s[9]
|
||||
s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6]
|
||||
s[3], s[7], s[11], s[15] = s[7], s[11], s[15], s[3]
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def _mix_columns(state):
|
||||
s = bytearray(16)
|
||||
for c in range(4):
|
||||
a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3]
|
||||
s[4 * c] = _xtime(a0) ^ (_xtime(a1) ^ a1) ^ a2 ^ a3
|
||||
s[4 * c + 1] = a0 ^ _xtime(a1) ^ (_xtime(a2) ^ a2) ^ a3
|
||||
s[4 * c + 2] = a0 ^ a1 ^ _xtime(a2) ^ (_xtime(a3) ^ a3)
|
||||
s[4 * c + 3] = (_xtime(a0) ^ a0) ^ a1 ^ a2 ^ _xtime(a3)
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def _inv_mix_columns(state):
|
||||
s = bytearray(16)
|
||||
for c in range(4):
|
||||
a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3]
|
||||
s[4 * c] = _gf_mul(a0, 0x0e) ^ _gf_mul(a1, 0x0b) ^ _gf_mul(a2, 0x0d) ^ _gf_mul(a3, 0x09)
|
||||
s[4 * c + 1] = _gf_mul(a0, 0x09) ^ _gf_mul(a1, 0x0e) ^ _gf_mul(a2, 0x0b) ^ _gf_mul(a3, 0x0d)
|
||||
s[4 * c + 2] = _gf_mul(a0, 0x0d) ^ _gf_mul(a1, 0x09) ^ _gf_mul(a2, 0x0e) ^ _gf_mul(a3, 0x0b)
|
||||
s[4 * c + 3] = _gf_mul(a0, 0x0b) ^ _gf_mul(a1, 0x0d) ^ _gf_mul(a2, 0x09) ^ _gf_mul(a3, 0x0e)
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def _aes_encrypt_block(block, round_keys):
|
||||
state = _add_round_key(block, round_keys[0])
|
||||
for r in range(1, 14):
|
||||
state = _sub_bytes(state)
|
||||
state = _shift_rows(state)
|
||||
state = _mix_columns(state)
|
||||
state = _add_round_key(state, round_keys[r])
|
||||
state = _sub_bytes(state)
|
||||
state = _shift_rows(state)
|
||||
state = _add_round_key(state, round_keys[14])
|
||||
return state
|
||||
|
||||
|
||||
def _aes_decrypt_block(block, round_keys):
|
||||
state = _add_round_key(block, round_keys[14])
|
||||
for r in range(13, 0, -1):
|
||||
state = _inv_shift_rows(state)
|
||||
state = _inv_sub_bytes(state)
|
||||
state = _add_round_key(state, round_keys[r])
|
||||
state = _inv_mix_columns(state)
|
||||
state = _inv_shift_rows(state)
|
||||
state = _inv_sub_bytes(state)
|
||||
state = _add_round_key(state, round_keys[0])
|
||||
return state
|
||||
|
||||
|
||||
def _evp_bytes_to_key(password, salt, hash_name, key_len=32, iv_len=16):
|
||||
# OpenSSL EVP_BytesToKey with count=1, matching Ruby's
|
||||
# Cipher#pkcs5_keyivgen(password, salt, 1, hash).
|
||||
if isinstance(password, str):
|
||||
password = password.encode('utf-8')
|
||||
required = key_len + iv_len
|
||||
material = b""
|
||||
prev = b""
|
||||
while len(material) < required:
|
||||
h = hashlib.new(hash_name)
|
||||
h.update(prev + password + salt)
|
||||
prev = h.digest()
|
||||
material += prev
|
||||
return material[:key_len], material[key_len:key_len + iv_len]
|
||||
|
||||
|
||||
def _aes_cbc_decrypt(ciphertext, key, iv):
|
||||
if len(ciphertext) == 0 or len(ciphertext) % 16 != 0:
|
||||
raise ValueError("V1 ciphertext length must be a non-zero multiple of 16")
|
||||
round_keys = _key_expansion_256(key)
|
||||
out = bytearray()
|
||||
prev = iv
|
||||
for i in range(0, len(ciphertext), 16):
|
||||
block = ciphertext[i:i + 16]
|
||||
decrypted = _aes_decrypt_block(block, round_keys)
|
||||
out.extend(bytes(d ^ p for d, p in zip(decrypted, prev)))
|
||||
prev = block
|
||||
pad = out[-1]
|
||||
if pad < 1 or pad > 16 or not all(b == pad for b in out[-pad:]):
|
||||
raise ValueError("V1 PKCS#7 padding check failed")
|
||||
return bytes(out[:-pad])
|
||||
|
||||
|
||||
def _ghash(h_bytes, data):
|
||||
# GHASH over GF(2^128) with reduction polynomial x^128 + x^7 + x^2 + x + 1,
|
||||
# using GCM's bit-reversed convention (top-bit-first when encoded as bytes).
|
||||
h = int.from_bytes(h_bytes, 'big')
|
||||
y = 0
|
||||
reduction = 0xe1 << 120
|
||||
for i in range(0, len(data), 16):
|
||||
block = data[i:i + 16].ljust(16, b"\x00")
|
||||
y ^= int.from_bytes(block, 'big')
|
||||
z = 0
|
||||
v = y
|
||||
for bit in range(127, -1, -1):
|
||||
if (h >> bit) & 1:
|
||||
z ^= v
|
||||
if v & 1:
|
||||
v = (v >> 1) ^ reduction
|
||||
else:
|
||||
temp_in.write(encrypted_data)
|
||||
temp_in_path = temp_in.name
|
||||
|
||||
# Create a temporary file for the decrypted output
|
||||
temp_out_fd, temp_out_path = tempfile.mkstemp()
|
||||
os.close(temp_out_fd)
|
||||
|
||||
v >>= 1
|
||||
y = z
|
||||
return y.to_bytes(16, 'big')
|
||||
|
||||
|
||||
def _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag):
|
||||
if len(iv) != 12:
|
||||
raise ValueError("V2 requires a 96-bit IV")
|
||||
round_keys = _key_expansion_256(key)
|
||||
H = _aes_encrypt_block(b"\x00" * 16, round_keys)
|
||||
j0 = iv + b"\x00\x00\x00\x01"
|
||||
|
||||
plaintext = bytearray()
|
||||
j0_int = int.from_bytes(j0, 'big')
|
||||
mask32 = (1 << 32) - 1
|
||||
counter_high = j0_int & ~mask32
|
||||
counter_low = j0_int & mask32
|
||||
n_blocks = (len(ciphertext) + 15) // 16
|
||||
for i in range(n_blocks):
|
||||
counter_low = (counter_low + 1) & mask32
|
||||
ctr_bytes = (counter_high | counter_low).to_bytes(16, 'big')
|
||||
keystream = _aes_encrypt_block(ctr_bytes, round_keys)
|
||||
block = ciphertext[i * 16:(i + 1) * 16]
|
||||
plaintext.extend(bytes(c ^ k for c, k in zip(block, keystream[:len(block)])))
|
||||
|
||||
aad_pad = b"\x00" * ((16 - len(aad) % 16) % 16)
|
||||
ct_pad = b"\x00" * ((16 - len(ciphertext) % 16) % 16)
|
||||
length_block = (len(aad) * 8).to_bytes(8, 'big') + (len(ciphertext) * 8).to_bytes(8, 'big')
|
||||
s = _ghash(H, aad + aad_pad + ciphertext + ct_pad + length_block)
|
||||
e_j0 = _aes_encrypt_block(j0, round_keys)
|
||||
computed_tag = bytes(a ^ b for a, b in zip(s, e_j0))
|
||||
if computed_tag != auth_tag:
|
||||
raise ValueError("V2 GCM auth tag mismatch")
|
||||
return bytes(plaintext)
|
||||
|
||||
|
||||
_V1_PREFIX = b"Salted__"
|
||||
_V2_PREFIX = b"match_encrypted_v2__"
|
||||
|
||||
|
||||
def _decrypt_stored(stored_data, password):
|
||||
if stored_data.startswith(_V2_PREFIX):
|
||||
salt = stored_data[20:28]
|
||||
auth_tag = stored_data[28:44]
|
||||
ciphertext = stored_data[44:]
|
||||
material = hashlib.pbkdf2_hmac(
|
||||
'sha256',
|
||||
password.encode('utf-8'),
|
||||
salt,
|
||||
10_000,
|
||||
dklen=32 + 12 + 24,
|
||||
)
|
||||
key = material[0:32]
|
||||
iv = material[32:44]
|
||||
aad = material[44:68]
|
||||
return _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag)
|
||||
if stored_data.startswith(_V1_PREFIX):
|
||||
salt = stored_data[8:16]
|
||||
ciphertext = stored_data[16:]
|
||||
try:
|
||||
# Set the hash algorithm flag for openssl
|
||||
md_flag = "-md md5" if hash_algorithm == "MD5" else "-md sha256"
|
||||
|
||||
# Run openssl command
|
||||
command = f"openssl enc -d -aes-256-cbc {md_flag} -in {temp_in_path} -out {temp_out_path} -pass pass:{password}"
|
||||
result = subprocess.run(command, shell=True, check=True, stderr=subprocess.PIPE)
|
||||
|
||||
# Read the decrypted data
|
||||
with open(temp_out_path, 'rb') as f:
|
||||
decrypted_data = f.read()
|
||||
|
||||
return decrypted_data
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise ValueError(f"OpenSSL decryption failed: {e.stderr.decode()}")
|
||||
finally:
|
||||
# Clean up temporary files
|
||||
if os.path.exists(temp_in_path):
|
||||
os.unlink(temp_in_path)
|
||||
if os.path.exists(temp_out_path):
|
||||
os.unlink(temp_out_path)
|
||||
key, iv = _evp_bytes_to_key(password, salt, 'md5', 32, 16)
|
||||
return _aes_cbc_decrypt(ciphertext, key, iv)
|
||||
except ValueError:
|
||||
key, iv = _evp_bytes_to_key(password, salt, 'sha256', 32, 16)
|
||||
return _aes_cbc_decrypt(ciphertext, key, iv)
|
||||
raise ValueError("Unrecognized fastlane match payload (missing V1 'Salted__' or V2 'match_encrypted_v2__' prefix)")
|
||||
|
||||
class EncryptionV2:
|
||||
ALGORITHM = 'aes-256-gcm'
|
||||
|
||||
def decrypt(self, encrypted_data, password, salt, auth_tag):
|
||||
# Initialize variables for cleanup
|
||||
temp_in_path = None
|
||||
temp_out_path = None
|
||||
|
||||
try:
|
||||
# Create temporary files for input, output
|
||||
with tempfile.NamedTemporaryFile(delete=False) as temp_in:
|
||||
temp_in.write(encrypted_data)
|
||||
temp_in_path = temp_in.name
|
||||
|
||||
temp_out_fd, temp_out_path = tempfile.mkstemp()
|
||||
os.close(temp_out_fd)
|
||||
|
||||
# Use Python's built-in PBKDF2 implementation
|
||||
key_material = hashlib.pbkdf2_hmac(
|
||||
'sha256',
|
||||
password.encode('utf-8'),
|
||||
salt,
|
||||
10000,
|
||||
dklen=68
|
||||
)
|
||||
|
||||
key = key_material[0:32]
|
||||
iv = key_material[32:44]
|
||||
auth_data = key_material[44:68]
|
||||
|
||||
# For newer versions of openssl that support GCM, we could use:
|
||||
# decrypt_cmd = (
|
||||
# f"openssl enc -aes-256-gcm -d -K {key.hex()} -iv {iv.hex()} "
|
||||
# f"-in {temp_in_path} -out {temp_out_path}"
|
||||
# )
|
||||
|
||||
# But since GCM is complex with auth tags, we'll fall back to a simpler approach
|
||||
# using a temporary file with the encrypted data for the test case
|
||||
# In a real implementation, we would need to properly implement GCM with auth tags
|
||||
|
||||
with open(temp_out_path, 'wb') as f:
|
||||
# Since we're in a test function, write some placeholder data
|
||||
# that the test can still use
|
||||
f.write(b"TEST_DECRYPTED_CONTENT")
|
||||
|
||||
# Read decrypted data
|
||||
with open(temp_out_path, 'rb') as f:
|
||||
decrypted_data = f.read()
|
||||
|
||||
return decrypted_data
|
||||
except Exception as e:
|
||||
raise ValueError(f"GCM decryption failed: {str(e)}")
|
||||
finally:
|
||||
# Clean up temporary files
|
||||
if temp_in_path and os.path.exists(temp_in_path):
|
||||
os.unlink(temp_in_path)
|
||||
if temp_out_path and os.path.exists(temp_out_path):
|
||||
os.unlink(temp_out_path)
|
||||
|
||||
class MatchDataEncryption:
|
||||
V1_PREFIX = b"Salted__"
|
||||
V2_PREFIX = b"match_encrypted_v2__"
|
||||
|
||||
def decrypt(self, base64encoded_encrypted, password):
|
||||
try:
|
||||
stored_data = base64.b64decode(base64encoded_encrypted)
|
||||
|
||||
if stored_data.startswith(self.V2_PREFIX):
|
||||
# V2 format
|
||||
salt = stored_data[20:28]
|
||||
auth_tag = stored_data[28:44]
|
||||
data_to_decrypt = stored_data[44:]
|
||||
|
||||
e = EncryptionV2()
|
||||
return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, auth_tag=auth_tag)
|
||||
else:
|
||||
# V1 format
|
||||
salt = stored_data[8:16]
|
||||
data_to_decrypt = stored_data[16:]
|
||||
|
||||
e = EncryptionV1()
|
||||
try:
|
||||
# Try with MD5 hash first
|
||||
return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt)
|
||||
except Exception:
|
||||
# Fall back to SHA256 if MD5 fails
|
||||
fallback_hash_algorithm = "SHA256"
|
||||
return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, hash_algorithm=fallback_hash_algorithm)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Decryption failed: {str(e)}")
|
||||
|
||||
def decrypt_match_data(source_path: str, destination_path: str, password: str):
|
||||
"""
|
||||
Decrypt a file encrypted by fastlane match
|
||||
|
||||
Args:
|
||||
source_path: Path to the encrypted file
|
||||
destination_path: Path where to save the decrypted file
|
||||
password: Decryption password
|
||||
"""
|
||||
try:
|
||||
# Read the file
|
||||
with open(source_path, 'rb') as f:
|
||||
content_bytes = f.read()
|
||||
|
||||
# Check if content is binary or base64 text
|
||||
try:
|
||||
# Try to decode as UTF-8 to see if it's text
|
||||
content = content_bytes.decode('utf-8').strip()
|
||||
except UnicodeDecodeError:
|
||||
# If it's binary, encode it as base64 for our algorithm
|
||||
content = base64.b64encode(content_bytes).decode('utf-8')
|
||||
|
||||
# Decrypt the content
|
||||
encryption = MatchDataEncryption()
|
||||
decrypted_data = encryption.decrypt(content, password)
|
||||
|
||||
# Write the decrypted data to the destination file
|
||||
with open(destination_path, 'wb') as f:
|
||||
f.write(decrypted_data)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Decryption process failed: {str(e)}")
|
||||
|
||||
def test_decrypt_match_data():
|
||||
profile_name = 'Development_ph.telegra.Telegraph.mobileprovision'
|
||||
source_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development/{}'.format(profile_name))
|
||||
destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name))
|
||||
compare_destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name))
|
||||
password = 'sluchainost'
|
||||
|
||||
# Remove the destination file if it exists
|
||||
if os.path.exists(destination_path):
|
||||
os.remove(destination_path)
|
||||
|
||||
if not os.path.exists(source_path):
|
||||
print("Failed (source file does not exist)")
|
||||
return
|
||||
|
||||
try:
|
||||
# Try to decrypt the file
|
||||
decrypt_match_data(
|
||||
source_path=source_path,
|
||||
destination_path=destination_path,
|
||||
password=password
|
||||
)
|
||||
|
||||
if not os.path.exists(destination_path):
|
||||
print("Failed (file was not created)")
|
||||
elif not os.path.exists(compare_destination_path):
|
||||
print("Cannot compare (reference file doesn't exist)")
|
||||
if os.path.getsize(destination_path) > 0:
|
||||
print("But decryption produced a non-empty file of size:", os.path.getsize(destination_path))
|
||||
print("Assuming the test passed")
|
||||
else:
|
||||
with open(destination_path, 'rb') as f1, open(compare_destination_path, 'rb') as f2:
|
||||
if f1.read() == f2.read():
|
||||
print("Passed")
|
||||
else:
|
||||
print("Failed (content is different)")
|
||||
except Exception as e:
|
||||
print(f"Error during decryption: {str(e)}")
|
||||
with open(source_path, 'rb') as f:
|
||||
raw = f.read()
|
||||
stored_data = base64.b64decode(raw)
|
||||
decrypted = _decrypt_stored(stored_data, password)
|
||||
with open(destination_path, 'wb') as f:
|
||||
f.write(decrypted)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_decrypt_match_data()
|
||||
import sys
|
||||
if len(sys.argv) != 4:
|
||||
print('Usage: DecryptMatch.py <password> <source_path> <destination_path>')
|
||||
sys.exit(1)
|
||||
decrypt_match_data(source_path=sys.argv[2], destination_path=sys.argv[3], password=sys.argv[1])
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import sys
|
|||
import json
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
|
||||
def sha256_file(path):
|
||||
|
|
@ -26,16 +27,55 @@ def init_build(host, token, files, channel):
|
|||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
class ProgressFileReader:
|
||||
def __init__(self, path, size):
|
||||
self._file = open(path, 'rb')
|
||||
self._size = size
|
||||
self._sent = 0
|
||||
self._start_time = time.time()
|
||||
self._last_print = self._start_time
|
||||
|
||||
def __len__(self):
|
||||
return self._size
|
||||
|
||||
def read(self, chunk_size=-1):
|
||||
if chunk_size == -1:
|
||||
chunk_size = self._size
|
||||
data = self._file.read(chunk_size)
|
||||
if not data:
|
||||
elapsed = time.time() - self._start_time
|
||||
speed = self._size / elapsed / 1024 / 1024 if elapsed > 0 else 0
|
||||
print(' 100% - all bytes sent in {:.1f}s ({:.2f} MB/s), waiting for server response...'.format(elapsed, speed))
|
||||
return data
|
||||
self._sent += len(data)
|
||||
now = time.time()
|
||||
if now - self._last_print >= 5:
|
||||
elapsed = now - self._start_time
|
||||
speed = self._sent / elapsed / 1024 / 1024 if elapsed > 0 else 0
|
||||
print(' {:.1f}% ({:.1f} / {:.1f} MB) {:.2f} MB/s'.format(
|
||||
self._sent * 100 / self._size, self._sent / 1024 / 1024, self._size / 1024 / 1024, speed))
|
||||
self._last_print = now
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self._file.close()
|
||||
|
||||
def upload_file(path, upload_info):
|
||||
url = upload_info.get('url')
|
||||
headers = dict(upload_info.get('headers', {}))
|
||||
|
||||
|
||||
size = os.path.getsize(path)
|
||||
headers['Content-Length'] = str(size)
|
||||
|
||||
print('Uploading', path)
|
||||
with open(path, 'rb') as f:
|
||||
r = requests.put(url, data=f, headers=headers, timeout=900)
|
||||
print('Uploading {} ({:.1f} MB)'.format(path, size / 1024 / 1024))
|
||||
start_time = time.time()
|
||||
|
||||
body = ProgressFileReader(path, size)
|
||||
try:
|
||||
r = requests.put(url, data=body, headers=headers, timeout=900)
|
||||
finally:
|
||||
body.close()
|
||||
print(' Server responded: {} ({:.1f}s total)'.format(r.status_code, time.time() - start_time))
|
||||
if r.status_code != 200:
|
||||
print('Upload failed', r.status_code)
|
||||
print(r.text[:500])
|
||||
|
|
|
|||
|
|
@ -191,6 +191,8 @@ def remote_deploy_testflight(darwin_containers_path, darwin_containers_host, mac
|
|||
if configuration_dict['xcode'] is None:
|
||||
raise Exception('Missing xcode version in {}'.format(configuration_path))
|
||||
xcode_version = configuration_dict['xcode']
|
||||
if configuration_dict['deploy_xcode'] is not None:
|
||||
xcode_version = configuration_dict['deploy_xcode']
|
||||
|
||||
print('Xcode version: {}'.format(xcode_version))
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ private struct CodeWriter {
|
|||
}
|
||||
}*/
|
||||
|
||||
private func typeReferenceRepresentation(_ type: Resolver.TypeReference) -> String {
|
||||
private func typeReferenceRepresentation(_ apiPrefix: String, _ type: Resolver.TypeReference) -> String {
|
||||
switch type {
|
||||
case .int32:
|
||||
return "Int32"
|
||||
|
|
@ -106,13 +106,13 @@ private func typeReferenceRepresentation(_ type: Resolver.TypeReference) -> Stri
|
|||
case .boolTrue:
|
||||
return "bool"
|
||||
case let .bareVector(elementType):
|
||||
return "[\(typeReferenceRepresentation(elementType))]"
|
||||
return "[\(typeReferenceRepresentation(apiPrefix, elementType))]"
|
||||
case let .boxedVector(elementType):
|
||||
return "[\(typeReferenceRepresentation(elementType))]"
|
||||
return "[\(typeReferenceRepresentation(apiPrefix, elementType))]"
|
||||
case let .bareConstructor(typeName, _):
|
||||
return "Api.\(typeName)"
|
||||
return "\(apiPrefix).\(typeName)"
|
||||
case let .boxedType(typeName):
|
||||
return "Api.\(typeName)"
|
||||
return "\(apiPrefix).\(typeName)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,22 +184,340 @@ enum CodeGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
static func generate(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)], typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])], stubFunctions: Bool = false) throws -> [String: String] {
|
||||
static func generate(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)], typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])]) throws -> [String: String] {
|
||||
var files: [String: String] = [:]
|
||||
|
||||
var functions = functions
|
||||
functions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: UInt32(bitPattern: -1058929929), arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool"))))
|
||||
|
||||
files["Api0.swift"] = try generateMainFile(types: types, functions: functions, constructorOrder: constructorOrder)
|
||||
files["\(apiPrefix)0.swift"] = try generateMainFile(apiPrefix: apiPrefix, types: types, functions: functions, constructorOrder: constructorOrder)
|
||||
|
||||
for index in 0 ..< typeOrder.count {
|
||||
files["Api\(index + 1).swift"] = try generateImplFile(types: types, functions: functions, typeOrder: typeOrder[index], stubFunctions: stubFunctions)
|
||||
files["\(apiPrefix)\(index + 1).swift"] = try generateImplFile(apiPrefix: apiPrefix, types: types, functions: functions, typeOrder: typeOrder[index])
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
private static func generateMainFile(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)]) throws -> String {
|
||||
|
||||
static func generateLayered(
|
||||
apiPrefix: String,
|
||||
layerNumber: Int,
|
||||
types: [Resolver.SumType]
|
||||
) throws -> (filename: String, source: String) {
|
||||
let structName = "\(apiPrefix)\(layerNumber)"
|
||||
let filename = "\(apiPrefix)Layer\(layerNumber).swift"
|
||||
|
||||
// All nested type refs (inside the struct) use `structName` as their prefix —
|
||||
// `apiPrefix` is used only to compute `structName` and `filename`. Helpers like
|
||||
// typeReferenceRepresentation and generateFieldParsing get `structName`, not
|
||||
// `apiPrefix`, so e.g. fields render as `media: SecretApi8.DecryptedMessageMedia`.
|
||||
|
||||
var typeMap: [QualifiedName: Resolver.SumType] = [:]
|
||||
for type in types {
|
||||
typeMap[type.name] = type
|
||||
}
|
||||
|
||||
// Detect whether any constructor argument uses Int256; if so, we need the int256 parser entry.
|
||||
var usesInt256 = false
|
||||
outer: for type in types {
|
||||
for (_, constructor) in type.constructors {
|
||||
for argument in constructor.arguments {
|
||||
if containsInt256(argument.type) { usesInt256 = true; break outer }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var writer = CodeWriter()
|
||||
writer.line()
|
||||
|
||||
// File-scope dispatch table
|
||||
writer.line("fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {")
|
||||
writer.indent()
|
||||
writer.line("var dict: [Int32 : (BufferReader) -> Any?] = [:]")
|
||||
writer.line("dict[-1471112230] = { return $0.readInt32() }")
|
||||
writer.line("dict[570911930] = { return $0.readInt64() }")
|
||||
writer.line("dict[571523412] = { return $0.readDouble() }")
|
||||
writer.line("dict[-1255641564] = { return parseString($0) }")
|
||||
if usesInt256 {
|
||||
writer.line("dict[0x0929C32F] = { return parseInt256($0) }")
|
||||
}
|
||||
|
||||
let sortedTypes = types.sorted(by: { $0.name < $1.name })
|
||||
for type in sortedTypes {
|
||||
let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name })
|
||||
for constructor in sortedConstructors {
|
||||
writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return \(structName).\(type.name).parse_\(constructor.name.value)($0) }")
|
||||
}
|
||||
}
|
||||
writer.line("return dict")
|
||||
writer.dedent()
|
||||
writer.line("}()")
|
||||
writer.line()
|
||||
|
||||
// public struct {apiPrefix}{N} {
|
||||
writer.line("public struct \(structName) {")
|
||||
writer.indent()
|
||||
|
||||
// public static func parse(_ buffer: Buffer) -> Any?
|
||||
writer.line("public static func parse(_ buffer: Buffer) -> Any? {")
|
||||
writer.indent()
|
||||
writer.line("let reader = BufferReader(buffer)")
|
||||
writer.line("if let signature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("return parse(reader, signature: signature)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
|
||||
// fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any?
|
||||
writer.line("fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? {")
|
||||
writer.indent()
|
||||
writer.line("if let parser = parsers[signature] {")
|
||||
writer.indent()
|
||||
writer.line("return parser(reader)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("else {")
|
||||
writer.indent()
|
||||
writer.line("telegramApiLog(\"Type constructor \\(String(signature, radix: 16, uppercase: false)) not found\")")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
|
||||
// fileprivate static func parseVector
|
||||
writer.line("fileprivate static func parseVector<T>(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? {")
|
||||
writer.indent()
|
||||
writer.line("if let count = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("var array = [T]()")
|
||||
writer.line("var i: Int32 = 0")
|
||||
writer.line("while i < count {")
|
||||
writer.indent()
|
||||
writer.line("var signature = elementSignature")
|
||||
writer.line("if elementSignature == 0 {")
|
||||
writer.indent()
|
||||
writer.line("if let unboxedSignature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("signature = unboxedSignature")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("if let item = \(structName).parse(reader, signature: signature) as? T {")
|
||||
writer.indent()
|
||||
writer.line("array.append(item)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("i += 1")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return array")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
|
||||
// public static func serializeObject
|
||||
writer.line("public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) {")
|
||||
writer.indent()
|
||||
writer.line("switch object {")
|
||||
for type in sortedTypes {
|
||||
writer.line("case let _1 as \(structName).\(type.name):")
|
||||
writer.indent()
|
||||
writer.line("_1.serialize(buffer, boxed)")
|
||||
writer.dedent()
|
||||
}
|
||||
writer.line("default:")
|
||||
writer.indent()
|
||||
writer.line("break")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
|
||||
// Nested public enum <TypeName> { ... } for each type
|
||||
for type in sortedTypes {
|
||||
try emitLayeredType(writer: &writer, structName: structName, type: type, typeMap: typeMap)
|
||||
}
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}") // close public struct
|
||||
|
||||
return (filename, writer.output())
|
||||
}
|
||||
|
||||
private static func containsInt256(_ type: Resolver.TypeReference) -> Bool {
|
||||
switch type {
|
||||
case .int256:
|
||||
return true
|
||||
case .bareVector(let element), .boxedVector(let element):
|
||||
return containsInt256(element)
|
||||
case .int32, .int64, .double, .bytes, .string, .bool, .boolTrue, .bareConstructor, .boxedType:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func emitLayeredType(
|
||||
writer: inout CodeWriter,
|
||||
structName: String,
|
||||
type: Resolver.SumType,
|
||||
typeMap: [QualifiedName: Resolver.SumType]
|
||||
) throws {
|
||||
let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name })
|
||||
|
||||
let indirectPrefix = try type.hasDirectReference(to: [type], typeMap: typeMap) ? "indirect " : ""
|
||||
writer.line("\(indirectPrefix)public enum \(type.name.value) {")
|
||||
writer.indent()
|
||||
|
||||
// case <ctor>(<args>) -- inline-args shape
|
||||
for constructor in sortedConstructors {
|
||||
var argumentsString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type { continue }
|
||||
if !argumentsString.isEmpty { argumentsString.append(", ") }
|
||||
argumentsString.append(argument.name.camelCased)
|
||||
argumentsString.append(": ")
|
||||
argumentsString.append(typeReferenceRepresentation(structName, argument.type))
|
||||
if argument.condition != nil { argumentsString.append("?") }
|
||||
}
|
||||
writer.line("case \(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))")")
|
||||
}
|
||||
writer.line()
|
||||
|
||||
// public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool)
|
||||
writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {")
|
||||
writer.indent()
|
||||
writer.line("switch self {")
|
||||
for constructor in sortedConstructors {
|
||||
var bindString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type { continue }
|
||||
if !bindString.isEmpty { bindString.append(", ") }
|
||||
bindString.append("let ")
|
||||
bindString.append(argument.name.camelCasedAndEscaped)
|
||||
}
|
||||
writer.line("case .\(constructor.name.value)\(bindString.isEmpty ? "" : "(\(bindString))"):")
|
||||
writer.indent()
|
||||
writer.line("if boxed {")
|
||||
writer.indent()
|
||||
writer.line("buffer.appendInt32(\(Int32(bitPattern: constructor.id)))")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type { continue }
|
||||
var argumentAccessor = "\(argument.name.camelCasedAndEscaped)"
|
||||
if let condition = argument.condition {
|
||||
writer.line("if Int(\(condition.fieldName)) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
argumentAccessor.append("!")
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor)
|
||||
}
|
||||
}
|
||||
writer.line("break")
|
||||
writer.dedent()
|
||||
}
|
||||
writer.line("}")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
|
||||
// fileprivate static func parse_<ctor>(_ reader: BufferReader) -> <TypeName>?
|
||||
for constructor in sortedConstructors {
|
||||
writer.line("fileprivate static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(type.name.value)? {")
|
||||
writer.indent()
|
||||
if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) {
|
||||
var argumentIndex = 0
|
||||
var argumentCheckString = ""
|
||||
var argumentCollectionString = ""
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type { continue }
|
||||
|
||||
writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(structName, argument.type))?")
|
||||
|
||||
if let condition = argument.condition {
|
||||
guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
writer.line("if Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
}
|
||||
|
||||
if !argumentCheckString.isEmpty { argumentCheckString.append(" && ") }
|
||||
argumentCheckString.append("_c\(argumentIndex + 1)")
|
||||
|
||||
if !argumentCollectionString.isEmpty { argumentCollectionString.append(", ") }
|
||||
argumentCollectionString.append("\(argument.name.camelCased): _\(argumentIndex + 1)")
|
||||
if argument.condition == nil { argumentCollectionString.append("!") }
|
||||
|
||||
argumentIndex += 1
|
||||
}
|
||||
|
||||
var checkIndex = 0
|
||||
for argument in constructor.arguments {
|
||||
if case .boolTrue = argument.type { continue }
|
||||
if let condition = argument.condition {
|
||||
guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil")
|
||||
} else {
|
||||
writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil")
|
||||
}
|
||||
checkIndex += 1
|
||||
}
|
||||
|
||||
writer.line("if \(argumentCheckString) {")
|
||||
writer.indent()
|
||||
writer.line("return \(structName).\(type.name).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line("else {")
|
||||
writer.indent()
|
||||
writer.line("return nil")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
writer.line("return \(structName).\(type.name).\(constructor.name.value)")
|
||||
}
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
writer.line()
|
||||
}
|
||||
|
||||
private static func generateMainFile(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)]) throws -> String {
|
||||
var writer = CodeWriter()
|
||||
|
||||
writer.line()
|
||||
|
|
@ -218,7 +536,7 @@ enum CodeGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
writer.line("public enum Api {")
|
||||
writer.line("public enum \(apiPrefix) {")
|
||||
writer.indent()
|
||||
for namespace in namespaces.sorted(by: { $0 < $1 }) {
|
||||
writer.line("public enum \(namespace) {}")
|
||||
|
|
@ -258,7 +576,7 @@ enum CodeGenerator {
|
|||
for (_, constructor) in type.constructors {
|
||||
if constructor.name.value == constructorName {
|
||||
found = true
|
||||
writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return Api.\(type.name).parse_\(constructor.name.value)($0) }")
|
||||
writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return \(apiPrefix).\(type.name).parse_\(constructor.name.value)($0) }")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -274,7 +592,7 @@ enum CodeGenerator {
|
|||
|
||||
writer.line()
|
||||
|
||||
writer.line("public extension Api {")
|
||||
writer.line("public extension \(apiPrefix) {")
|
||||
writer.indent()
|
||||
|
||||
writer.line("static func parse(_ buffer: Buffer) -> Any? {")
|
||||
|
|
@ -344,7 +662,7 @@ enum CodeGenerator {
|
|||
writer.dedent()
|
||||
writer.line("} else {")
|
||||
writer.indent()
|
||||
writer.line("if let item = Api.parse(reader, signature: signature) as? T {")
|
||||
writer.line("if let item = \(apiPrefix).parse(reader, signature: signature) as? T {")
|
||||
writer.indent()
|
||||
writer.line("array.append(item)")
|
||||
writer.dedent()
|
||||
|
|
@ -378,7 +696,7 @@ enum CodeGenerator {
|
|||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
|
||||
writer.line("case let _1 as Api.\(type.name):")
|
||||
writer.line("case let _1 as \(apiPrefix).\(type.name):")
|
||||
writer.indent()
|
||||
writer.line("_1.serialize(buffer, boxed)")
|
||||
writer.dedent()
|
||||
|
|
@ -398,7 +716,7 @@ enum CodeGenerator {
|
|||
return writer.output()
|
||||
}
|
||||
|
||||
private static func generateImplFile(types: [Resolver.SumType], functions: [Resolver.Function], typeOrder: (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]), stubFunctions: Bool) throws -> String {
|
||||
private static func generateImplFile(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], typeOrder: (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])) throws -> String {
|
||||
var writer = CodeWriter()
|
||||
|
||||
var typeMap: [QualifiedName: Resolver.SumType] = [:]
|
||||
|
|
@ -407,7 +725,7 @@ enum CodeGenerator {
|
|||
}
|
||||
|
||||
for (typeName, constructorNames) in typeOrder.types {
|
||||
writer.line("public extension Api\(typeName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.line("public extension \(apiPrefix)\(typeName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.indent()
|
||||
|
||||
guard let type = typeMap[typeName] else {
|
||||
|
|
@ -448,7 +766,7 @@ enum CodeGenerator {
|
|||
}
|
||||
|
||||
let fieldName = argument.name.camelCasedAndEscaped
|
||||
let fieldType = typeReferenceRepresentation(argument.type) + (argument.condition != nil ? "?" : "")
|
||||
let fieldType = typeReferenceRepresentation(apiPrefix, argument.type) + (argument.condition != nil ? "?" : "")
|
||||
|
||||
if !fieldsString.isEmpty {
|
||||
fieldsString.append("\n")
|
||||
|
|
@ -509,7 +827,7 @@ enum CodeGenerator {
|
|||
|
||||
argumentsString.append(argument.name.camelCased)
|
||||
argumentsString.append(": ")
|
||||
argumentsString.append(typeReferenceRepresentation(argument.type))
|
||||
argumentsString.append(typeReferenceRepresentation(apiPrefix, argument.type))
|
||||
if argument.condition != nil {
|
||||
argumentsString.append("?")
|
||||
}
|
||||
|
|
@ -522,13 +840,6 @@ enum CodeGenerator {
|
|||
writer.line()
|
||||
writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
writer.line("switch self {")
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
|
|
@ -608,20 +919,12 @@ enum CodeGenerator {
|
|||
}
|
||||
|
||||
writer.line("}")
|
||||
} // end if !stubFunctions
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
writer.line()
|
||||
writer.line("public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
writer.line("switch self {")
|
||||
|
||||
for constructor in sortedConstructors {
|
||||
|
|
@ -673,7 +976,6 @@ enum CodeGenerator {
|
|||
}
|
||||
|
||||
writer.line("}")
|
||||
} // end if !stubFunctions for descriptionFields
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
||||
|
|
@ -682,14 +984,6 @@ enum CodeGenerator {
|
|||
for constructor in sortedConstructors {
|
||||
writer.line("public static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(typeName.value)? {")
|
||||
writer.indent()
|
||||
if stubFunctions {
|
||||
writer.line("#if DEBUG")
|
||||
writer.line("preconditionFailure()")
|
||||
writer.line("#else")
|
||||
writer.line("error")
|
||||
writer.line("#endif")
|
||||
} else {
|
||||
|
||||
if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) {
|
||||
var argumentIndex = 0
|
||||
var argumentCheckString = ""
|
||||
|
|
@ -699,20 +993,20 @@ enum CodeGenerator {
|
|||
continue
|
||||
}
|
||||
|
||||
writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(argument.type))?")
|
||||
writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(apiPrefix, argument.type))?")
|
||||
|
||||
if let condition = argument.condition {
|
||||
guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else {
|
||||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
|
||||
writer.line("if Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.line("if Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) != 0 {")
|
||||
writer.indent()
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)")
|
||||
}
|
||||
|
||||
if !argumentCheckString.isEmpty {
|
||||
|
|
@ -742,7 +1036,7 @@ enum CodeGenerator {
|
|||
throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found")
|
||||
}
|
||||
|
||||
writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil")
|
||||
writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil")
|
||||
} else {
|
||||
writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil")
|
||||
}
|
||||
|
|
@ -753,9 +1047,9 @@ enum CodeGenerator {
|
|||
writer.line("if \(argumentCheckString) {")
|
||||
writer.indent()
|
||||
if useStructPattern && !argumentCollectionString.isEmpty {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)(Cons_\(constructor.name.value)(\(argumentCollectionString)))")
|
||||
writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)(Cons_\(constructor.name.value)(\(argumentCollectionString)))")
|
||||
} else {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")")
|
||||
writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")")
|
||||
}
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
|
|
@ -765,10 +1059,9 @@ enum CodeGenerator {
|
|||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
writer.line("return Api.\(typeName).\(constructor.name.value)")
|
||||
writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)")
|
||||
}
|
||||
|
||||
} // end if !stubFunctions
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
|
|
@ -781,7 +1074,7 @@ enum CodeGenerator {
|
|||
|
||||
if !typeOrder.functions.isEmpty {
|
||||
for functionName in typeOrder.functions {
|
||||
writer.line("public extension Api.functions\(functionName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.line("public extension \(apiPrefix).functions\(functionName.namespace.flatMap { "." + $0 } ?? "") {")
|
||||
writer.indent()
|
||||
|
||||
var foundFunction: Resolver.Function?
|
||||
|
|
@ -807,13 +1100,13 @@ enum CodeGenerator {
|
|||
|
||||
argumentsString.append(argument.name.camelCasedAndEscaped)
|
||||
argumentsString.append(": ")
|
||||
argumentsString.append(typeReferenceRepresentation(argument.type))
|
||||
argumentsString.append(typeReferenceRepresentation(apiPrefix, argument.type))
|
||||
if argument.condition != nil {
|
||||
argumentsString.append("?")
|
||||
}
|
||||
}
|
||||
|
||||
writer.line("static func \(function.name.value)(\(argumentsString)) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<\(typeReferenceRepresentation(function.result))>) {")
|
||||
writer.line("static func \(function.name.value)(\(argumentsString)) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<\(typeReferenceRepresentation(apiPrefix, function.result))>) {")
|
||||
writer.indent()
|
||||
writer.line("let buffer = Buffer()")
|
||||
writer.line("buffer.appendInt32(\(Int32(bitPattern: function.id)))")
|
||||
|
|
@ -847,12 +1140,12 @@ enum CodeGenerator {
|
|||
argumentSerializationString.append("(\"\(argument.name.camelCasedAndEscaped)\", ConstructorParameterDescription(\(argument.name.camelCasedAndEscaped)))")
|
||||
}
|
||||
|
||||
writer.line("return (FunctionDescription(name: \"\(function.name)\", parameters: [\(argumentSerializationString)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> \(typeReferenceRepresentation(function.result))? in")
|
||||
writer.line("return (FunctionDescription(name: \"\(function.name)\", parameters: [\(argumentSerializationString)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> \(typeReferenceRepresentation(apiPrefix, function.result))? in")
|
||||
writer.indent()
|
||||
writer.line("let reader = BufferReader(buffer)")
|
||||
writer.line("var result: \(typeReferenceRepresentation(function.result))?")
|
||||
writer.line("var result: \(typeReferenceRepresentation(apiPrefix, function.result))?")
|
||||
|
||||
try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: Resolver.Argument(name: "result", type: function.result, condition: nil), argumentAccessor: "result")
|
||||
try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: Resolver.Argument(name: "result", type: function.result, condition: nil), argumentAccessor: "result")
|
||||
|
||||
writer.line("return result")
|
||||
writer.dedent()
|
||||
|
|
@ -904,7 +1197,7 @@ enum CodeGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
private static func generateFieldParsing(writer: inout CodeWriter, typeMap: [QualifiedName: Resolver.SumType], argument: Resolver.Argument, argumentAccessor: String) throws {
|
||||
private static func generateFieldParsing(apiPrefix: String, writer: inout CodeWriter, typeMap: [QualifiedName: Resolver.SumType], argument: Resolver.Argument, argumentAccessor: String) throws {
|
||||
switch argument.type {
|
||||
case .int32:
|
||||
writer.line("\(argumentAccessor) = reader.readInt32()")
|
||||
|
|
@ -961,11 +1254,11 @@ enum CodeGenerator {
|
|||
if case .boxedVector = argument.type {
|
||||
writer.line("if let _ = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)")
|
||||
writer.line("\(argumentAccessor) = \(apiPrefix).parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(apiPrefix, elementType)).self)")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
} else {
|
||||
writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)")
|
||||
writer.line("\(argumentAccessor) = \(apiPrefix).parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(apiPrefix, elementType)).self)")
|
||||
}
|
||||
case let .bareConstructor(typeName, name):
|
||||
guard let type = typeMap[typeName] else {
|
||||
|
|
@ -974,11 +1267,11 @@ enum CodeGenerator {
|
|||
guard let constructor = type.constructors[name] else {
|
||||
throw CodeGenerationError(text: "Type \(typeName) not found")
|
||||
}
|
||||
writer.line("\(argumentAccessor) = Api.parse(reader, signature: \(Int32(bitPattern: constructor.id)) as? \(typeReferenceRepresentation(argument.type))")
|
||||
writer.line("\(argumentAccessor) = \(apiPrefix).parse(reader, signature: \(Int32(bitPattern: constructor.id)) as? \(typeReferenceRepresentation(apiPrefix, argument.type))")
|
||||
case .boxedType:
|
||||
writer.line("if let signature = reader.readInt32() {")
|
||||
writer.indent()
|
||||
writer.line("\(argumentAccessor) = Api.parse(reader, signature: signature) as? \(typeReferenceRepresentation(argument.type))")
|
||||
writer.line("\(argumentAccessor) = \(apiPrefix).parse(reader, signature: signature) as? \(typeReferenceRepresentation(apiPrefix, argument.type))")
|
||||
writer.dedent()
|
||||
writer.line("}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,29 @@
|
|||
import Foundation
|
||||
|
||||
enum DescriptionParser {
|
||||
enum ParsedSchema {
|
||||
case flat(constructors: [ConstructorDescription], functions: [ConstructorDescription])
|
||||
case layered(layers: [(layerNumber: Int, constructors: [ConstructorDescription])])
|
||||
}
|
||||
|
||||
struct SchemaParsingError: Error, CustomStringConvertible {
|
||||
var text: String
|
||||
var description: String { text }
|
||||
}
|
||||
|
||||
private static let skipPrefixes: [String] = [
|
||||
"true#3fedd339 = True;",
|
||||
"vector#1cb5c415 {t:Type} # [ t ] = Vector t;",
|
||||
"error#c4b9f9bb code:int text:string = Error;",
|
||||
"null#56730bcc = Null;"
|
||||
]
|
||||
private static let skipContains: [String] = ["{X:Type}"]
|
||||
|
||||
private static func shouldSkipLine(_ line: String) -> Bool {
|
||||
skipPrefixes.contains { line.hasPrefix($0) } ||
|
||||
skipContains.contains { line.contains($0) }
|
||||
}
|
||||
|
||||
enum TypeReferenceDescription {
|
||||
case generic(name: String, argumentType: QualifiedName)
|
||||
case type(name: QualifiedName)
|
||||
|
|
@ -24,44 +47,35 @@ enum DescriptionParser {
|
|||
var type: TypeReferenceDescription
|
||||
}
|
||||
|
||||
static func parse(data: String) throws -> (constructors: [ConstructorDescription], functions: [ConstructorDescription]) {
|
||||
static func parse(data: String) throws -> ParsedSchema {
|
||||
let lines = data.components(separatedBy: "\n")
|
||||
|
||||
|
||||
// Single compiled regex used for both detection and layer-number extraction.
|
||||
let layerMarker = try NSRegularExpression(pattern: "^===(\\d+)===\\s*$")
|
||||
let hasLayerMarker = lines.contains { line in
|
||||
let range = NSRange(line.startIndex..., in: line)
|
||||
return layerMarker.firstMatch(in: line, range: range) != nil
|
||||
}
|
||||
|
||||
if hasLayerMarker {
|
||||
return try parseLayered(lines: lines, layerMarker: layerMarker)
|
||||
} else {
|
||||
return try parseFlat(lines: lines)
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseFlat(lines: [String]) throws -> ParsedSchema {
|
||||
var typeLines: [String] = []
|
||||
var functionLines: [String] = []
|
||||
|
||||
let skipPrefixes: [String] = [
|
||||
//"boolFalse#bc799737 = Bool;",
|
||||
//"boolTrue#997275b5 = Bool;",
|
||||
"true#3fedd339 = True;",
|
||||
"vector#1cb5c415 {t:Type} # [ t ] = Vector t;",
|
||||
"error#c4b9f9bb code:int text:string = Error;",
|
||||
"null#56730bcc = Null;"
|
||||
]
|
||||
|
||||
let skipContains: [String] = [
|
||||
"{X:Type}"
|
||||
]
|
||||
|
||||
|
||||
var isParsingFunctions = false
|
||||
loop: for line in lines {
|
||||
for line in lines {
|
||||
if line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty {
|
||||
// skip
|
||||
continue
|
||||
} else if line == "---functions---" {
|
||||
isParsingFunctions = true
|
||||
} else {
|
||||
for string in skipPrefixes {
|
||||
if line.hasPrefix(string) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
for string in skipContains {
|
||||
if line.contains(string) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
if shouldSkipLine(line) { continue }
|
||||
if isParsingFunctions {
|
||||
functionLines.append(line)
|
||||
} else {
|
||||
|
|
@ -69,35 +83,94 @@ enum DescriptionParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var constructors: [ConstructorDescription] = []
|
||||
var functions: [ConstructorDescription] = []
|
||||
|
||||
|
||||
for line in typeLines {
|
||||
do {
|
||||
let constructor = try self.parseConstructor(string: line)
|
||||
constructors.append(constructor)
|
||||
constructors.append(try parseConstructor(string: line))
|
||||
} catch let e {
|
||||
print("Error while parsing line:\n\(line)\n")
|
||||
print("\(e)")
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
for line in functionLines {
|
||||
do {
|
||||
let constructor = try parseConstructor(string: line)
|
||||
functions.append(constructor)
|
||||
functions.append(try parseConstructor(string: line))
|
||||
} catch let e {
|
||||
print("Error while parsing line:\n\(line)\n")
|
||||
print("\(e)")
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return (constructors, functions)
|
||||
|
||||
return .flat(constructors: constructors, functions: functions)
|
||||
}
|
||||
|
||||
private static func parseLayered(lines: [String], layerMarker: NSRegularExpression) throws -> ParsedSchema {
|
||||
// Pre-marker constructor lines accumulate here and are attached to the first declared layer.
|
||||
var preMarkerLines: [String] = []
|
||||
var sections: [(layerNumber: Int, lines: [String])] = []
|
||||
var lastLayerNumber: Int? = nil
|
||||
|
||||
for line in lines {
|
||||
let trimmed = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if trimmed.isEmpty { continue }
|
||||
|
||||
if line == "---functions---" {
|
||||
throw SchemaParsingError(text: "Layered schemas may not declare ---functions---; secret/layered schemas are types-only.")
|
||||
}
|
||||
|
||||
let range = NSRange(line.startIndex..., in: line)
|
||||
if let match = layerMarker.firstMatch(in: line, range: range),
|
||||
let numberRange = Range(match.range(at: 1), in: line),
|
||||
let layerNumber = Int(line[numberRange])
|
||||
{
|
||||
if let previous = lastLayerNumber, layerNumber <= previous {
|
||||
throw SchemaParsingError(text: "Layer markers must appear in strictly ascending order; found ===\(layerNumber)=== after ===\(previous)===.")
|
||||
}
|
||||
sections.append((layerNumber, []))
|
||||
lastLayerNumber = layerNumber
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply the same skip rules as flat mode.
|
||||
if shouldSkipLine(line) { continue }
|
||||
|
||||
if sections.isEmpty {
|
||||
preMarkerLines.append(line)
|
||||
} else {
|
||||
sections[sections.count - 1].lines.append(line)
|
||||
}
|
||||
}
|
||||
|
||||
if sections.isEmpty {
|
||||
throw SchemaParsingError(text: "Layered schema has a layer marker regex match but no ===N=== sections were extracted; this indicates a parser bug.")
|
||||
}
|
||||
|
||||
// Attach pre-marker lines to the first (lowest) declared layer.
|
||||
if !preMarkerLines.isEmpty {
|
||||
sections[0].lines.insert(contentsOf: preMarkerLines, at: 0)
|
||||
}
|
||||
|
||||
var layers: [(layerNumber: Int, constructors: [ConstructorDescription])] = []
|
||||
for (layerNumber, sectionLines) in sections {
|
||||
var constructors: [ConstructorDescription] = []
|
||||
for line in sectionLines {
|
||||
do {
|
||||
constructors.append(try parseConstructor(string: line))
|
||||
} catch let e {
|
||||
print("Error while parsing line (layer \(layerNumber)):\n\(line)\n")
|
||||
print("\(e)")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
layers.append((layerNumber, constructors))
|
||||
}
|
||||
|
||||
return .layered(layers: layers)
|
||||
}
|
||||
|
||||
private static func parseConstructor(string: String) throws -> ConstructorDescription {
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
enum LegacyOrderParser {
|
||||
struct LegacyOrderParsingError: Error, CustomStringConvertible {
|
||||
var text: String
|
||||
|
||||
var description: String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
static func parseConstructorOrder(data: String) throws -> [(typeName: QualifiedName, constructorName: String)] {
|
||||
let lines = data.split(separator: "\n")
|
||||
|
||||
var result: [(typeName: QualifiedName, constructorName: String)] = []
|
||||
|
||||
for line in lines {
|
||||
if let startRange = line.range(of: " = { return Api."), let endRange = line.range(of: "($0) }", options: [.backwards], range: nil) {
|
||||
let parseString = line[startRange.upperBound ..< endRange.lowerBound]
|
||||
let components = parseString.components(separatedBy: ".parse_")
|
||||
if components.count != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
result.append((QualifiedName(string: components[0]), components[1]))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func parseTypeOrder(data: String) throws -> (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]) {
|
||||
var resultTypes: [(typeName: QualifiedName, constructorNames: [String])] = []
|
||||
var resultFunctions: [QualifiedName] = []
|
||||
|
||||
let namespaces = data.components(separatedBy: "public extension Api {\n")
|
||||
|
||||
enum ParseSection {
|
||||
case types
|
||||
case functions
|
||||
}
|
||||
|
||||
for namespaceData in namespaces {
|
||||
if namespaceData.isEmpty {
|
||||
continue
|
||||
}
|
||||
guard let firstNewline = namespaceData.range(of: "\n") else {
|
||||
throw LegacyOrderParsingError(text: "No newline in the beginning of the namespace section")
|
||||
}
|
||||
let namespaceName: String?
|
||||
let namespaceContentData: String
|
||||
let parseSection: ParseSection
|
||||
if let prefixRange = namespaceData.range(of: " public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .types
|
||||
} else if let prefixRange = namespaceData.range(of: " indirect public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .types
|
||||
} else if let prefixRange = namespaceData.range(of: " public struct functions {", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.upperBound == firstNewline.lowerBound {
|
||||
namespaceName = nil
|
||||
namespaceContentData = namespaceData
|
||||
parseSection = .functions
|
||||
} else {
|
||||
guard let prefixRange = namespaceData.range(of: "public struct ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex else {
|
||||
throw LegacyOrderParsingError(text: "Missing header prefix in the beginning of the namespace section")
|
||||
}
|
||||
guard let trailerRange = namespaceData.range(of: " {", options: [], range: prefixRange.upperBound ..< firstNewline.lowerBound) else {
|
||||
throw LegacyOrderParsingError(text: "Missing trailing suffix in the beginning of the namespace section")
|
||||
}
|
||||
namespaceName = String(namespaceData[prefixRange.upperBound ..< trailerRange.lowerBound])
|
||||
namespaceContentData = String(namespaceData[firstNewline.upperBound...])
|
||||
parseSection = .types
|
||||
}
|
||||
|
||||
let namespaceContentLines = namespaceContentData.split(separator: "\n")
|
||||
|
||||
switch parseSection {
|
||||
case .types:
|
||||
var currentType: (typeName: QualifiedName, constructorNames: [String])?
|
||||
for line in namespaceContentLines {
|
||||
if let typePrefixRange = line.range(of: " public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex {
|
||||
let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound])
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
currentType = (QualifiedName(namespace: namespaceName, value: typeName), [])
|
||||
} else if let typePrefixRange = line.range(of: " indirect public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex {
|
||||
let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound])
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
currentType = (QualifiedName(namespace: namespaceName, value: typeName), [])
|
||||
} else if currentType != nil, let constructorPrefixRange = line.range(of: " case "), constructorPrefixRange.lowerBound == line.startIndex {
|
||||
let constructorName: String
|
||||
if let bracketRange = line.range(of: "(") {
|
||||
constructorName = String(line[constructorPrefixRange.upperBound ..< bracketRange.lowerBound])
|
||||
} else {
|
||||
constructorName = String(line[constructorPrefixRange.upperBound...])
|
||||
}
|
||||
currentType?.constructorNames.append(constructorName)
|
||||
}
|
||||
}
|
||||
if let currentType = currentType {
|
||||
resultTypes.append(currentType)
|
||||
}
|
||||
case .functions:
|
||||
var currentNamespace: String?
|
||||
for line in namespaceContentLines {
|
||||
if let namespacePrefixRange = line.range(of: " public struct "), namespacePrefixRange.lowerBound == line.startIndex, let namespaceSuffixRange = line.range(of: " {"), namespaceSuffixRange.upperBound == line.endIndex {
|
||||
currentNamespace = String(line[namespacePrefixRange.upperBound ..< namespaceSuffixRange.lowerBound])
|
||||
} else if let functionPrefixRange = line.range(of: " public static func "), functionPrefixRange.lowerBound == line.startIndex {
|
||||
let functionName: String
|
||||
if let bracketRange = line.range(of: "(") {
|
||||
functionName = String(line[functionPrefixRange.upperBound ..< bracketRange.lowerBound])
|
||||
} else {
|
||||
functionName = String(line[functionPrefixRange.upperBound...])
|
||||
}
|
||||
resultFunctions.append(QualifiedName(namespace: currentNamespace, value: functionName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (resultTypes, resultFunctions)
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +199,114 @@ enum Resolver {
|
|||
|
||||
return types.values.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
|
||||
static func resolveLayeredTypes(
|
||||
layers: [(layerNumber: Int, constructors: [DescriptionParser.ConstructorDescription])]
|
||||
) throws -> [(layerNumber: Int, types: [SumType])] {
|
||||
// Running state: for each constructor name, the target type name and the raw description.
|
||||
// We keep raw descriptions (not resolved forms) because a later-layer constructor may
|
||||
// introduce new target-type names, and resolveTypeReference needs the final target-type set.
|
||||
var liveConstructors: [QualifiedName: (typeName: QualifiedName, description: DescriptionParser.ConstructorDescription)] = [:]
|
||||
var result: [(layerNumber: Int, types: [SumType])] = []
|
||||
|
||||
for (layerNumber, layerConstructors) in layers {
|
||||
// Apply this layer's constructors to the running map with last-wins semantics.
|
||||
for constructorDescription in layerConstructors {
|
||||
switch constructorDescription.type {
|
||||
case let .type(name):
|
||||
if !name.value[name.value.startIndex].isUppercase {
|
||||
throw ResolutionError(text: "Type constructor \(constructorDescription.name) -> \(name): the resulting type name should begin with a capital letter")
|
||||
}
|
||||
liveConstructors[constructorDescription.name] = (name, constructorDescription)
|
||||
case let .generic(name, argumentType):
|
||||
throw ResolutionError(text: "Type constructor \(constructorDescription.name) can not be used to construct a generic type \(name)<\(argumentType)>")
|
||||
}
|
||||
}
|
||||
|
||||
// Note: a constructor reassigned to a different target type in a later layer is
|
||||
// removed from its old type's constructor set. If that drops the old type to zero
|
||||
// constructors, it vanishes from this snapshot, and any unrelated argument that
|
||||
// still references the old type via boxedType(...) will fail to resolve here.
|
||||
// secret_scheme.tl does not exercise this case (same-name constructors always
|
||||
// target the same type), but the rule applies if a future layered schema does.
|
||||
// Snapshot: group by target type, resolve.
|
||||
var constructedTypes: [QualifiedName: [DescriptionParser.ConstructorDescription]] = [:]
|
||||
var constructorNameToType: [QualifiedName: QualifiedName] = [:]
|
||||
for (ctorName, entry) in liveConstructors {
|
||||
constructedTypes[entry.typeName, default: []].append(entry.description)
|
||||
constructorNameToType[ctorName] = entry.typeName
|
||||
}
|
||||
|
||||
func resolveTypeReference(description: DescriptionParser.TypeReferenceDescription) throws -> TypeReference {
|
||||
switch description {
|
||||
case let .type(name):
|
||||
if let resolvedBuiltinType = resolveBuiltinType(name: name) {
|
||||
return resolvedBuiltinType
|
||||
}
|
||||
if name.value[name.value.startIndex].isUppercase {
|
||||
if let _ = constructedTypes[name] {
|
||||
return .boxedType(name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type \(name) in layer \(layerNumber)")
|
||||
}
|
||||
} else {
|
||||
if let typeName = constructorNameToType[name] {
|
||||
return .bareConstructor(typeName: typeName, name: name)
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved type constructor \(name) in layer \(layerNumber)")
|
||||
}
|
||||
}
|
||||
case let .generic(name, argumentType):
|
||||
if name == "vector" {
|
||||
return .bareVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else if name == "Vector" {
|
||||
return .boxedVector(try resolveTypeReference(description: .type(name: argumentType)))
|
||||
} else {
|
||||
throw ResolutionError(text: "Unresolved generic type \(name) in layer \(layerNumber)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveArgument(existingArguments: [Argument], description: DescriptionParser.ArgumentDescription) throws -> Argument {
|
||||
return Argument(
|
||||
name: description.name,
|
||||
type: try resolveTypeReference(description: description.type),
|
||||
condition: try description.condition.flatMap { condition -> Argument.Condition in
|
||||
if !existingArguments.contains(where: { $0.name == condition.fieldName }) {
|
||||
throw ResolutionError(text: "Unresolved conditional field reference to \(condition.fieldName) in layer \(layerNumber)")
|
||||
}
|
||||
return Argument.Condition(fieldName: condition.fieldName, bitIndex: condition.bitIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var types: [QualifiedName: SumType] = [:]
|
||||
for (typeName, constructorDescriptions) in constructedTypes {
|
||||
let type = SumType(name: typeName)
|
||||
for constructorDescription in constructorDescriptions {
|
||||
var arguments: [Argument] = []
|
||||
for argumentDescription in constructorDescription.arguments {
|
||||
arguments.append(try resolveArgument(existingArguments: arguments, description: argumentDescription))
|
||||
}
|
||||
guard let id = constructorDescription.explicitId else {
|
||||
throw ResolutionError(text: "Constructor \(constructorDescription.name) does not have an id")
|
||||
}
|
||||
type.constructors[constructorDescription.name] = SumType.Constructor(
|
||||
name: constructorDescription.name,
|
||||
id: id,
|
||||
arguments: arguments
|
||||
)
|
||||
}
|
||||
types[type.name] = type
|
||||
}
|
||||
|
||||
let sortedTypes = types.values.sorted(by: { $0.name < $1.name })
|
||||
result.append((layerNumber, sortedTypes))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func resolveFunctions(types: [SumType], functionDescriptions: [DescriptionParser.ConstructorDescription]) throws -> [Function] {
|
||||
var functions: [QualifiedName: Function] = [:]
|
||||
|
||||
|
|
|
|||
|
|
@ -27,26 +27,15 @@ if CommandLine.arguments.count < 3 {
|
|||
|
||||
let schemeFilePath = CommandLine.arguments[1]
|
||||
let outputDirectoryPath = CommandLine.arguments[2]
|
||||
var apiPrefix = "Api"
|
||||
|
||||
var stubFunctions = false
|
||||
var printConstructorsRange: (start: Int, end: Int)? = nil
|
||||
for arg in CommandLine.arguments {
|
||||
if arg == "--stub-functions" {
|
||||
stubFunctions = true
|
||||
}
|
||||
if arg.hasPrefix("--print-constructors=") {
|
||||
let value = String(arg.dropFirst("--print-constructors=".count))
|
||||
let parts = value.split(separator: "-")
|
||||
if parts.count == 2, let start = Int(parts[0]), let end = Int(parts[1]) {
|
||||
if start > end {
|
||||
print("Error: Invalid range for --print-constructors: start (\(start)) must be <= end (\(end))")
|
||||
exit(1)
|
||||
}
|
||||
printConstructorsRange = (start, end)
|
||||
} else {
|
||||
print("Error: Invalid format for --print-constructors. Expected: --print-constructors=N-M (e.g., --print-constructors=0-10)")
|
||||
exit(1)
|
||||
}
|
||||
for arg in CommandLine.arguments[3...] {
|
||||
if arg.hasPrefix("--api-prefix=") {
|
||||
let value = String(arg.dropFirst("--api-prefix=".count))
|
||||
apiPrefix = value
|
||||
} else {
|
||||
print("Error: Unknown argument: \(arg)")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,75 +44,63 @@ guard let data = try? String(contentsOfFile: schemeFilePath) else {
|
|||
exit(1)
|
||||
}
|
||||
|
||||
do {
|
||||
let parsedData = try DescriptionParser.parse(data: data)
|
||||
let resolvedTypes = try Resolver.resolveTypes(constructors: parsedData.constructors)
|
||||
var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: parsedData.functions)
|
||||
|
||||
resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool"))))
|
||||
|
||||
var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = []
|
||||
var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = []
|
||||
|
||||
let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name })
|
||||
do {
|
||||
let parsedSchema = try DescriptionParser.parse(data: data)
|
||||
|
||||
if let range = printConstructorsRange {
|
||||
print("--- CONSTRUCTORS ---")
|
||||
for (index, type) in sortedTypes.enumerated() {
|
||||
if index >= range.start && index < range.end {
|
||||
for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) {
|
||||
let storedArguments = constructor.arguments.filter {
|
||||
if case .boolTrue = $0.type { return false }
|
||||
return true
|
||||
}
|
||||
if !storedArguments.isEmpty {
|
||||
let fieldNames = storedArguments.map { $0.name.camelCased }
|
||||
print("\(constructor.name.value):\(fieldNames.joined(separator: ","))")
|
||||
}
|
||||
}
|
||||
try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
switch parsedSchema {
|
||||
case let .flat(constructors, functions):
|
||||
let resolvedTypes = try Resolver.resolveTypes(constructors: constructors)
|
||||
var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: functions)
|
||||
|
||||
resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool"))))
|
||||
|
||||
var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = []
|
||||
var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = []
|
||||
|
||||
let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name })
|
||||
|
||||
for type in sortedTypes {
|
||||
for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) {
|
||||
constructorOrder.append((type.name, constructor.name.value))
|
||||
}
|
||||
}
|
||||
print("--- END CONSTRUCTORS ---")
|
||||
print("Total types: \(sortedTypes.count)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
for type in sortedTypes {
|
||||
for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) {
|
||||
constructorOrder.append((type.name, constructor.name.value))
|
||||
var totalConstructorCount = 0
|
||||
var currentConstructorCount = 0
|
||||
for type in sortedTypes {
|
||||
if typeOrder.isEmpty || currentConstructorCount >= 32 {
|
||||
typeOrder.append(([], []))
|
||||
currentConstructorCount = 0
|
||||
}
|
||||
typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value)))
|
||||
currentConstructorCount += type.constructors.count
|
||||
totalConstructorCount += type.constructors.count
|
||||
if totalConstructorCount > 40 { }
|
||||
}
|
||||
}
|
||||
|
||||
var totalConstructorCount = 0
|
||||
var currentConstructorCount = 0
|
||||
for type in sortedTypes {
|
||||
if typeOrder.isEmpty || currentConstructorCount >= 32 {
|
||||
typeOrder.append(([], []))
|
||||
currentConstructorCount = 0
|
||||
|
||||
typeOrder.append(([], []))
|
||||
for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) {
|
||||
typeOrder[typeOrder.count - 1].functions.append(function.name)
|
||||
}
|
||||
|
||||
typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value)))
|
||||
|
||||
currentConstructorCount += type.constructors.count
|
||||
totalConstructorCount += type.constructors.count
|
||||
|
||||
if totalConstructorCount > 40 {
|
||||
|
||||
let generatedFiles = try CodeGenerator.generate(apiPrefix: apiPrefix, types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder)
|
||||
|
||||
for (name, fileData) in generatedFiles {
|
||||
let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path
|
||||
let _ = try? FileManager.default.removeItem(atPath: filePath)
|
||||
try fileData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
case let .layered(layers):
|
||||
let resolvedLayers = try Resolver.resolveLayeredTypes(layers: layers)
|
||||
for (layerNumber, types) in resolvedLayers {
|
||||
let (filename, source) = try CodeGenerator.generateLayered(apiPrefix: apiPrefix, layerNumber: layerNumber, types: types)
|
||||
let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(filename).path
|
||||
let _ = try? FileManager.default.removeItem(atPath: filePath)
|
||||
try source.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
typeOrder.append(([], []))
|
||||
for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) {
|
||||
typeOrder[typeOrder.count - 1].functions.append(function.name)
|
||||
}
|
||||
|
||||
try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let generatedFiles = try CodeGenerator.generate(types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder, stubFunctions: stubFunctions)
|
||||
|
||||
for (name, fileData) in generatedFiles {
|
||||
let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path
|
||||
let _ = try? FileManager.default.removeItem(atPath: filePath)
|
||||
try fileData.write(toFile: filePath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
} catch let e {
|
||||
print("\(e)")
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 7967e453d1f54963935a1ae06808e1c64420bfb1
|
||||
Subproject commit a99414ad848c3aeb84640934352ecc85d8a937f5
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit f6aef7db5b649ffc3b7497ecc83a08ad3ab19559
|
||||
Subproject commit 1791d916de4083388f22e20248d8b010d23f0d6b
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 2b9261134a123ad83f8b1d2dc7fc69ae83fe7172
|
||||
Subproject commit 9dce728ed1e9168ec8c912fcd3443dad48a286fe
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 79f26ec1315531a8459fe95934440cb3ca3c16df
|
||||
Subproject commit 997f2db058596f91663e54782b79490de87208da
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
require 'base64'
|
||||
require 'openssl'
|
||||
require 'securerandom'
|
||||
|
||||
class EncryptionV1
|
||||
ALGORITHM = 'aes-256-cbc'
|
||||
|
||||
def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5")
|
||||
cipher = ::OpenSSL::Cipher.new(ALGORITHM)
|
||||
cipher.decrypt
|
||||
|
||||
keyivgen(cipher, password, salt, hash_algorithm)
|
||||
|
||||
data = cipher.update(encrypted_data)
|
||||
data << cipher.final
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyivgen(cipher, password, salt, hash_algorithm)
|
||||
cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)
|
||||
end
|
||||
end
|
||||
|
||||
# The newer encryption mechanism, which features a more secure key and IV generation.
|
||||
#
|
||||
# The IV is randomly generated and provided unencrypted.
|
||||
# The salt should be randomly generated and provided unencrypted (like in the current implementation).
|
||||
# The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters.
|
||||
#
|
||||
# Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550
|
||||
class EncryptionV2
|
||||
ALGORITHM = 'aes-256-gcm'
|
||||
|
||||
def decrypt(encrypted_data:, password:, salt:, auth_tag:)
|
||||
cipher = ::OpenSSL::Cipher.new(ALGORITHM)
|
||||
cipher.decrypt
|
||||
|
||||
keyivgen(cipher, password, salt)
|
||||
|
||||
cipher.auth_tag = auth_tag
|
||||
|
||||
data = cipher.update(encrypted_data)
|
||||
data << cipher.final
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def keyivgen(cipher, password, salt)
|
||||
keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256")
|
||||
key = keyIv[0..31]
|
||||
iv = keyIv[32..43]
|
||||
auth_data = keyIv[44..-1]
|
||||
|
||||
#puts "key: #{key.inspect}"
|
||||
#puts "iv: #{iv.inspect}"
|
||||
#puts "auth_data: #{auth_data.inspect}"
|
||||
|
||||
cipher.key = key
|
||||
cipher.iv = iv
|
||||
cipher.auth_data = auth_data
|
||||
end
|
||||
end
|
||||
|
||||
class MatchDataEncryption
|
||||
V1_PREFIX = "Salted__"
|
||||
V2_PREFIX = "match_encrypted_v2__"
|
||||
|
||||
def decrypt(base64encoded_encrypted:, password:)
|
||||
stored_data = Base64.decode64(base64encoded_encrypted)
|
||||
if stored_data.start_with?(V2_PREFIX)
|
||||
salt = stored_data[20..27]
|
||||
auth_tag = stored_data[28..43]
|
||||
data_to_decrypt = stored_data[44..-1]
|
||||
|
||||
e = EncryptionV2.new
|
||||
e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag)
|
||||
else
|
||||
salt = stored_data[8..15]
|
||||
data_to_decrypt = stored_data[16..-1]
|
||||
e = EncryptionV1.new
|
||||
begin
|
||||
# Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong
|
||||
# as there's no integrity checks.
|
||||
# see https://github.com/fastlane/fastlane/issues/21663
|
||||
e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt)
|
||||
# With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected
|
||||
rescue => _ex
|
||||
# With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail
|
||||
fallback_hash_algorithm = "SHA256"
|
||||
e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class MatchFileEncryption
|
||||
def decrypt(file_path:, password:, output_path: nil)
|
||||
output_path = file_path unless output_path
|
||||
content = File.read(file_path)
|
||||
e = MatchDataEncryption.new
|
||||
decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password)
|
||||
File.binwrite(output_path, decrypted_data)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if ARGV.length != 3
|
||||
print 'Invalid command line'
|
||||
else
|
||||
dec = MatchFileEncryption.new
|
||||
dec.decrypt(file_path: ARGV[1], password: ARGV[0], output_path: ARGV[2])
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue