This commit is contained in:
恍兮惚兮 2025-01-10 07:55:05 +08:00
parent 73c7276cde
commit da34e9d3e1
11 changed files with 1567 additions and 1993 deletions

View File

@ -83,6 +83,8 @@
* [uyjulian/AtlasTranslate](https://github.com/uyjulian/AtlasTranslate)
* [ilius/pyglossary](https://github.com/ilius/pyglossary)
</details>

View File

@ -592,6 +592,13 @@ namespace
strReplace(ws, L"^", L"");
buffer->from(WideStringToString(ws, 932));
}
void PCSG01151(TextBuffer *buffer, HookParam *hp)
{
auto ws = StringToWideString(buffer->viewA(), 932).value();
strReplace(ws, L"^", L"");
strReplace(ws, L" ", L"");
buffer->from(WideStringToString(ws, 932));
}
void FPCSG01066(TextBuffer *buffer, HookParam *hp)
{
auto s = buffer->strA();
@ -828,6 +835,8 @@ namespace
// DRAMAtical Murder
{0x8004630a, {0, 0, 0, 0, FPCSG00852, "PCSG00420"}},
{0x8003eed2, {0, 0, 0, 0, FPCSG00852, "PCSG00420"}},
// GALTIA V Edition
{0x8001B7AA, {0, 0, 0, 0, PCSG01151, "PCSG01151"}},
};
return 1;

View File

@ -1,7 +1,7 @@
set(VERSION_MAJOR 6)
set(VERSION_MINOR 19)
set(VERSION_PATCH 3)
set(VERSION_PATCH 4)
set(VERSION_REVISION 0)
set(LUNA_VERSION "{${VERSION_MAJOR},${VERSION_MINOR},${VERSION_PATCH},${VERSION_REVISION}}")
add_library(VERSION_DEF ${CMAKE_CURRENT_LIST_DIR}/version_def.cpp)

View File

@ -54,11 +54,12 @@ function onclickbtn_xxxxxx_internal(_id) {
}
.tab-widget_xxxxxx_internal .tab-button_xxxx_internal {
padding: 10px 20px;
padding: 7px 15px;
background-color: #cccccccc;
border: none;
cursor: pointer;
display: inline-block;
line-height: 20px;
}
.tab-widget_xxxxxx_internal .tab-button_xxxx_internal.active {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
from .readmdict import MDD, MDX
from . import lzo

View File

@ -0,0 +1,242 @@
import math
class FlexBuffer():
def __init__(self):
self.blockSize = None
self.c = None
self.l = None
self.buf = None
def require(self, n):
r = self.c - self.l + n
if r > 0:
self.l = self.l + self.blockSize * math.ceil(r / self.blockSize)
# tmp = bytearray(self.l)
# for i in len(self.buf):
# tmp[i] = self.buf[i]
# self.buf = tmp
self.buf = self.buf + bytearray(self.l - len(self.buf))
self.c = self.c + n
return self.buf
def alloc(self, initSize, blockSize):
if blockSize:
sz = blockSize
else:
sz = 4096
self.blockSize = self.roundUp(sz)
self.c = 0
self.l = self.roundUp(initSize) | 0
self.l += self.blockSize - (self.l % self.blockSize)
self.buf = bytearray(self.l)
return self.buf
def roundUp(self, n):
r = n % 4
if r == 0:
return n
else:
return n + 4 - r
def reset(self):
self.c = 0
self.l = len(self.buf)
def pack(self, size):
return self.buf[0:size]
def _decompress(inBuf, outBuf):
c_top_loop = 1
c_first_literal_run = 2
c_match = 3
c_copy_match = 4
c_match_done = 5
c_match_next = 6
out = outBuf.buf
op = 0
ip = 0
t = inBuf[ip]
state = c_top_loop
m_pos = 0
ip_end = len(inBuf)
if t > 17:
ip = ip + 1
t = t - 17
if t < 4:
state = c_match_next
else:
out = outBuf.require(t)
while True:
out[op] = inBuf[ip]
op = op + 1
ip = ip + 1
t = t - 1
if not t > 0:
break
state = c_first_literal_run
while True:
if_block = False
##
if state == c_top_loop:
t = inBuf[ip]
ip = ip + 1
if t >= 16:
state = c_match
continue
if t == 0:
while inBuf[ip] == 0:
t = t + 255
ip = ip + 1
t = t + 15 + inBuf[ip]
ip = ip + 1
t = t + 3
out = outBuf.require(t)
while True:
out[op] = inBuf[ip]
op = op + 1
ip = ip + 1
t = t - 1
if not t > 0:
break
# emulate c switch
state = c_first_literal_run
##
if state == c_first_literal_run:
t = inBuf[ip]
ip = ip + 1
if t >= 16:
state = c_match
continue
m_pos = op - 0x801 - (t >> 2) - (inBuf[ip] << 2)
ip = ip + 1
out = outBuf.require(3)
out[op] = out[m_pos]
op = op + 1
m_pos = m_pos + 1
out[op] = out[m_pos]
op = op + 1
m_pos = m_pos + 1
out[op] = out[m_pos]
op = op + 1
state = c_match_done
continue
##
if state == c_match:
if t >= 64:
m_pos = op - 1 - ((t >> 2) & 7) - (inBuf[ip] << 3)
ip = ip + 1
t = (t >> 5) - 1
state = c_copy_match
continue
elif t >= 32:
t = t & 31
if t == 0:
while inBuf[ip] == 0:
t = t + 255
ip = ip + 1
t = t + 31 + inBuf[ip]
ip = ip + 1
m_pos = op - 1 - ((inBuf[ip] + (inBuf[ip + 1] << 8)) >> 2)
ip = ip + 2
elif t >= 16:
m_pos = op - ((t & 8) << 11)
t = t & 7
if t == 0:
while inBuf[ip] == 0:
t = t + 255
ip = ip + 1
t = t + 7 + inBuf[ip]
ip = ip + 1
m_pos = m_pos - ((inBuf[ip] + (inBuf[ip + 1] << 8)) >> 2)
ip = ip + 2
if m_pos == op:
break
m_pos = m_pos - 0x4000
else:
m_pos = op - 1 - (t >> 2) - (inBuf[ip] << 2)
ip = ip + 1
out = outBuf.require(2)
out[op] = out[m_pos]
op = op + 1
m_pos = m_pos + 1
out[op] = out[m_pos]
op = op + 1
state = c_match_done
continue
if t >= 6 and (op - m_pos) >= 4:
if_block = True
t += 2
out = outBuf.require(t)
while True:
out[op] = out[m_pos]
op += 1
m_pos += 1
t -= 1
if not t > 0:
break
# emulate c switch
state = c_copy_match
##
if state == c_copy_match:
if not if_block:
t += 2
out = outBuf.require(t)
while True:
out[op] = out[m_pos]
op += 1
m_pos += 1
t -= 1
if not t > 0:
break
# emulating c switch
state = c_match_done
##
if state == c_match_done:
t = inBuf[ip - 2] & 3
if t == 0:
state = c_top_loop
continue
# emulate c switch
state = c_match_next
##
if state == c_match_next:
out = outBuf.require(1)
out[op] = inBuf[ip]
op += 1
ip += 1
if t > 1:
out = outBuf.require(1)
out[op] = inBuf[ip]
op += 1
ip += 1
if t > 2:
out = outBuf.require(1)
out[op] = inBuf[ip]
op += 1
ip += 1
t = inBuf[ip]
ip += 1
state = c_match
continue
return bytes(outBuf.pack(op))
def decompress(input, initSize=16000, blockSize=8192):
output = FlexBuffer()
output.alloc(initSize, blockSize)
return _decompress(bytearray(input), output)

View File

@ -0,0 +1,363 @@
# coding: utf-8
# mypy: ignore-errors
# Copyright (C) 2016-2023 Saeed Rasooli on https://github.com/ilius/pyglossary/
# Copyright (C) 2015 Z. H. Liu on https://github.com/zhansliu/writemdict
# pureSalsa20.py -- a pure Python implementation of the Salsa20 cipher,
# ported to Python 3
# v4.0: Added Python 3 support, dropped support for Python <= 2.5.
# // zhansliu
# Original comments below.
# ====================================================================
# There are comments here by two authors about three pieces of software:
# comments by Larry Bugbee about
# Salsa20, the stream cipher by Daniel J. Bernstein
# (including comments about the speed of the C version) and
# pySalsa20, Bugbee's own Python wrapper for salsa20.c
# (including some references), and
# comments by Steve Witham about
# pureSalsa20, Witham's pure Python 2.5 implementation of Salsa20,
# which follows pySalsa20's API, and is in this file.
# Salsa20: a Fast Streaming Cipher (comments by Larry Bugbee)
# -----------------------------------------------------------
# Salsa20 is a fast stream cipher written by Daniel Bernstein
# that basically uses a hash function and XOR making for fast
# encryption. (Decryption uses the same function.) Salsa20
# is simple and quick.
# Some Salsa20 parameter values...
# design strength 128 bits
# key length 128 or 256 bits, exactly
# IV, aka nonce 64 bits, always
# chunk size must be in multiples of 64 bytes
# Salsa20 has two reduced versions, 8 and 12 rounds each.
# One benchmark (10 MB):
# 1.5GHz PPC G4 102/97/89 MB/sec for 8/12/20 rounds
# AMD Athlon 2500+ 77/67/53 MB/sec for 8/12/20 rounds
# (no I/O and before Python GC kicks in)
# Salsa20 is a Phase 3 finalist in the EU eSTREAM competition
# and appears to be one of the fastest ciphers. It is well
# documented so I will not attempt any injustice here. Please
# see "References" below.
# ...and Salsa20 is "free for any use".
# pySalsa20: a Python wrapper for Salsa20 (Comments by Larry Bugbee)
# ------------------------------------------------------------------
# pySalsa20.py is a simple ctypes Python wrapper. Salsa20 is
# as it's name implies, 20 rounds, but there are two reduced
# versions, 8 and 12 rounds each. Because the APIs are
# identical, pySalsa20 is capable of wrapping all three
# versions (number of rounds hardcoded), including a special
# version that allows you to set the number of rounds with a
# set_rounds() function. Compile the version of your choice
# as a shared library (not as a Python extension), name and
# install it as libsalsa20.so.
# Sample usage:
# from pySalsa20 import Salsa20
# s20 = Salsa20(key, IV)
# dataout = s20.encryptBytes(datain) # same for decrypt
# This is EXPERIMENTAL software and intended for educational
# purposes only. To make experimentation less cumbersome,
# pySalsa20 is also free for any use.
# THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF
# ANY KIND. USE AT YOUR OWN RISK.
# Enjoy,
# Larry Bugbee
# bugbee@seanet.com
# April 2007
# References:
# -----------
# http://en.wikipedia.org/wiki/Salsa20
# http://en.wikipedia.org/wiki/Daniel_Bernstein
# http://cr.yp.to/djb.html
# http://www.ecrypt.eu.org/stream/salsa20p3.html
# http://www.ecrypt.eu.org/stream/p3ciphers/salsa20/salsa20_p3source.zip
# Prerequisites for pySalsa20:
# ----------------------------
# - Python 2.5 (haven't tested in 2.4)
# pureSalsa20: Salsa20 in pure Python 2.5 (comments by Steve Witham)
# ------------------------------------------------------------------
# pureSalsa20 is the stand-alone Python code in this file.
# It implements the underlying Salsa20 core algorithm
# and emulates pySalsa20's Salsa20 class API (minus a bug(*)).
# pureSalsa20 is MUCH slower than libsalsa20.so wrapped with pySalsa20--
# about 1/1000 the speed for Salsa20/20 and 1/500 the speed for Salsa20/8,
# when encrypting 64k-byte blocks on my computer.
# pureSalsa20 is for cases where portability is much more important than
# speed. I wrote it for use in a "structured" random number generator.
# There are comments about the reasons for this slowness in
# http://www.tiac.net/~sw/2010/02/PureSalsa20
# Sample usage:
# from pureSalsa20 import Salsa20
# s20 = Salsa20(key, IV)
# dataout = s20.encryptBytes(datain) # same for decrypt
# I took the test code from pySalsa20, added a bunch of tests including
# rough speed tests, and moved them into the file testSalsa20.py.
# To test both pySalsa20 and pureSalsa20, type
# python testSalsa20.py
# (*)The bug (?) in pySalsa20 is this. The rounds variable is global to the
# libsalsa20.so library and not switched when switching between instances
# of the Salsa20 class.
# s1 = Salsa20( key, IV, 20 )
# s2 = Salsa20( key, IV, 8 )
# In this example,
# with pySalsa20, both s1 and s2 will do 8 rounds of encryption.
# with pureSalsa20, s1 will do 20 rounds and s2 will do 8 rounds.
# Perhaps giving each instance its own nRounds variable, which
# is passed to the salsa20wordtobyte() function, is insecure. I'm not a
# cryptographer.
# pureSalsa20.py and testSalsa20.py are EXPERIMENTAL software and
# intended for educational purposes only. To make experimentation less
# cumbersome, pureSalsa20.py and testSalsa20.py are free for any use.
# Revisions:
# ----------
# p3.2 Fixed bug that initialized the output buffer with plaintext!
# Saner ramping of nreps in speed test.
# Minor changes and print statements.
# p3.1 Took timing variability out of add32() and rot32().
# Made the internals more like pySalsa20/libsalsa .
# Put the semicolons back in the main loop!
# In encryptBytes(), modify a byte array instead of appending.
# Fixed speed calculation bug.
# Used subclasses instead of patches in testSalsa20.py .
# Added 64k-byte messages to speed test to be fair to pySalsa20.
# p3 First version, intended to parallel pySalsa20 version 3.
# More references:
# ----------------
# http://www.seanet.com/~bugbee/crypto/salsa20/ [pySalsa20]
# http://cr.yp.to/snuffle.html [The original name of Salsa20]
# http://cr.yp.to/snuffle/salsafamily-20071225.pdf [ Salsa20 design]
# http://www.tiac.net/~sw/2010/02/PureSalsa20
# THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF
# ANY KIND. USE AT YOUR OWN RISK.
# Cheers,
# Steve Witham sw at remove-this tiac dot net
# February, 2010
import operator
from struct import Struct
__all__ = ["Salsa20"]
little_u64 = Struct("<Q") # little-endian 64-bit unsigned.
# Unpacks to a tuple of one element!
little16_i32 = Struct("<16i") # 16 little-endian 32-bit signed ints.
little4_i32 = Struct("<4i") # 4 little-endian 32-bit signed ints.
little2_i32 = Struct("<2i") # 2 little-endian 32-bit signed ints.
_version = "p4.0"
# ----------- Salsa20 class which emulates pySalsa20.Salsa20 ---------------
class Salsa20:
def __init__(self, key=None, IV=None, rounds=20) -> None:
self._lastChunk64 = True
self._IVbitlen = 64 # must be 64 bits
self.ctx = [0] * 16
if key:
self.setKey(key)
if IV:
self.setIV(IV)
self.setRounds(rounds)
def setKey(self, key):
assert isinstance(key, bytes)
ctx = self.ctx
if len(key) == 32: # recommended
constants = b"expand 32-byte k"
ctx[1], ctx[2], ctx[3], ctx[4] = little4_i32.unpack(key[0:16])
ctx[11], ctx[12], ctx[13], ctx[14] = little4_i32.unpack(key[16:32])
elif len(key) == 16:
constants = b"expand 16-byte k"
ctx[1], ctx[2], ctx[3], ctx[4] = little4_i32.unpack(key[0:16])
ctx[11], ctx[12], ctx[13], ctx[14] = little4_i32.unpack(key[0:16])
else:
raise ValueError("key length isn't 32 or 16 bytes.")
ctx[0], ctx[5], ctx[10], ctx[15] = little4_i32.unpack(constants)
def setIV(self, IV):
assert isinstance(IV, bytes)
assert len(IV) * 8 == 64, "nonce (IV) not 64 bits"
self.IV = IV
ctx = self.ctx
ctx[6], ctx[7] = little2_i32.unpack(IV)
ctx[8], ctx[9] = 0, 0 # Reset the block counter.
setNonce = setIV # support an alternate name
def setCounter(self, counter):
assert isinstance(counter, int)
assert 0 <= counter < 1 << 64, "counter < 0 or >= 2**64"
ctx = self.ctx
ctx[8], ctx[9] = little2_i32.unpack(little_u64.pack(counter))
def getCounter(self):
return little_u64.unpack(little2_i32.pack(*self.ctx[8:10]))[0]
def setRounds(self, rounds, testing=False):
assert testing or rounds in {8, 12, 20}, "rounds must be 8, 12, 20"
self.rounds = rounds
def encryptBytes(self, data: bytes) -> bytes:
assert isinstance(data, bytes), "data must be byte string"
assert self._lastChunk64, "previous chunk not multiple of 64 bytes"
lendata = len(data)
munged = bytearray(lendata)
for i in range(0, lendata, 64):
h = salsa20_wordtobyte(self.ctx, self.rounds, checkRounds=False)
self.setCounter((self.getCounter() + 1) % 2**64)
# Stopping at 2^70 bytes per nonce is user's responsibility.
for j in range(min(64, lendata - i)):
munged[i + j] = data[i + j] ^ h[j]
self._lastChunk64 = not lendata % 64
return bytes(munged)
decryptBytes = encryptBytes # encrypt and decrypt use same function
# --------------------------------------------------------------------------
def salsa20_wordtobyte(input_, nRounds=20, checkRounds=True):
"""
Do nRounds Salsa20 rounds on a copy of
input: list or tuple of 16 ints treated as little-endian unsigneds.
Returns a 64-byte string.
"""
assert isinstance(input_, list | tuple) and len(input_) == 16
assert not checkRounds or nRounds in {8, 12, 20}
x = list(input_)
XOR = operator.xor
ROTATE = rot32
PLUS = add32
for _ in range(nRounds // 2):
# These ...XOR...ROTATE...PLUS... lines are from ecrypt-linux.c
# unchanged except for indents and the blank line between rounds:
x[4] = XOR(x[4], ROTATE(PLUS(x[0], x[12]), 7))
x[8] = XOR(x[8], ROTATE(PLUS(x[4], x[0]), 9))
x[12] = XOR(x[12], ROTATE(PLUS(x[8], x[4]), 13))
x[0] = XOR(x[0], ROTATE(PLUS(x[12], x[8]), 18))
x[9] = XOR(x[9], ROTATE(PLUS(x[5], x[1]), 7))
x[13] = XOR(x[13], ROTATE(PLUS(x[9], x[5]), 9))
x[1] = XOR(x[1], ROTATE(PLUS(x[13], x[9]), 13))
x[5] = XOR(x[5], ROTATE(PLUS(x[1], x[13]), 18))
x[14] = XOR(x[14], ROTATE(PLUS(x[10], x[6]), 7))
x[2] = XOR(x[2], ROTATE(PLUS(x[14], x[10]), 9))
x[6] = XOR(x[6], ROTATE(PLUS(x[2], x[14]), 13))
x[10] = XOR(x[10], ROTATE(PLUS(x[6], x[2]), 18))
x[3] = XOR(x[3], ROTATE(PLUS(x[15], x[11]), 7))
x[7] = XOR(x[7], ROTATE(PLUS(x[3], x[15]), 9))
x[11] = XOR(x[11], ROTATE(PLUS(x[7], x[3]), 13))
x[15] = XOR(x[15], ROTATE(PLUS(x[11], x[7]), 18))
x[1] = XOR(x[1], ROTATE(PLUS(x[0], x[3]), 7))
x[2] = XOR(x[2], ROTATE(PLUS(x[1], x[0]), 9))
x[3] = XOR(x[3], ROTATE(PLUS(x[2], x[1]), 13))
x[0] = XOR(x[0], ROTATE(PLUS(x[3], x[2]), 18))
x[6] = XOR(x[6], ROTATE(PLUS(x[5], x[4]), 7))
x[7] = XOR(x[7], ROTATE(PLUS(x[6], x[5]), 9))
x[4] = XOR(x[4], ROTATE(PLUS(x[7], x[6]), 13))
x[5] = XOR(x[5], ROTATE(PLUS(x[4], x[7]), 18))
x[11] = XOR(x[11], ROTATE(PLUS(x[10], x[9]), 7))
x[8] = XOR(x[8], ROTATE(PLUS(x[11], x[10]), 9))
x[9] = XOR(x[9], ROTATE(PLUS(x[8], x[11]), 13))
x[10] = XOR(x[10], ROTATE(PLUS(x[9], x[8]), 18))
x[12] = XOR(x[12], ROTATE(PLUS(x[15], x[14]), 7))
x[13] = XOR(x[13], ROTATE(PLUS(x[12], x[15]), 9))
x[14] = XOR(x[14], ROTATE(PLUS(x[13], x[12]), 13))
x[15] = XOR(x[15], ROTATE(PLUS(x[14], x[13]), 18))
for idx, item in enumerate(input_):
x[idx] = PLUS(x[idx], item)
return little16_i32.pack(*x)
# --------------------------- 32-bit ops -------------------------------
def trunc32(w):
"""
Return the bottom 32 bits of w as a Python int.
This creates longs temporarily, but returns an int.
"""
w = int((w & 0x7FFFFFFF) | -(w & 0x80000000))
assert isinstance(w, int)
return w
def add32(a, b):
"""
Add two 32-bit words discarding carry above 32nd bit,
and without creating a Python long.
Timing shouldn't vary.
"""
lo = (a & 0xFFFF) + (b & 0xFFFF)
hi = (a >> 16) + (b >> 16) + (lo >> 16)
return (-(hi & 0x8000) | (hi & 0x7FFF)) << 16 | (lo & 0xFFFF)
def rot32(w, nLeft):
"""
Rotate 32-bit word left by nLeft or right by -nLeft
without creating a Python long.
Timing depends on nLeft but not on w.
"""
nLeft &= 31 # which makes nLeft >= 0
if nLeft == 0:
return w
# Note: now 1 <= nLeft <= 31.
# RRRsLLLLLL There are nLeft RRR's, (31-nLeft) LLLLLL's,
# => sLLLLLLRRR and one s which becomes the sign bit.
RRR = ((w >> 1) & 0x7FFFFFFF) >> (31 - nLeft)
sLLLLLL = -((1 << (31 - nLeft)) & w) | (0x7FFFFFFF >> nLeft) & w
return RRR | (sLLLLLL << nLeft)
# --------------------------------- end -----------------------------------

View File

@ -0,0 +1,748 @@
# -*- coding: utf-8 -*-
# mypy: ignore-errors
#
# readmdict.py from https://bitbucket.org/xwang/mdict-analysis
# Octopus MDict Dictionary File (.mdx) and Resource File (.mdd) Analyser
#
# Copyright (C) 2016-2023 Saeed Rasooli on https://github.com/ilius/pyglossary/
# Copyright (C) 2012, 2013, 2015, 2022 Xiaoqiang Wang <xiaoqiangwang AT gmail DOT com>
#
# This program is a free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# You can get a copy of GNU General Public License along this program
# But you can always get it from http://www.gnu.org/licenses/gpl.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
import logging
import re
import sys
# zlib compression is used for engine version >=2.0
import zlib
from io import BytesIO
from struct import pack, unpack
from .pureSalsa20 import Salsa20
from .ripemd128 import ripemd128
# LZO compression is used for engine version < 2.0
# try:
# import lzo
# except ImportError:
# lzo = None
from . import lzo
# xxhash is used for engine version >= 3.0
try:
import xxhash
except ImportError:
xxhash = None
__all__ = ["MDD", "MDX"]
log = logging.getLogger(__name__)
def _unescape_entities(text):
"""Unescape offending tags < > " &."""
text = text.replace(b"&lt;", b"<")
text = text.replace(b"&gt;", b">")
text = text.replace(b"&quot;", b'"')
text = text.replace(b"&amp;", b"&")
return text # noqa: RET504
def _fast_decrypt(data, key):
"""XOR decryption."""
b = bytearray(data)
key = bytearray(key)
previous = 0x36
for i, bi in enumerate(b):
t = (bi >> 4 | bi << 4) & 0xFF
t = t ^ previous ^ (i & 0xFF) ^ key[i % len(key)]
previous = bi
b[i] = t
return bytes(b)
def _salsa_decrypt(ciphertext, encrypt_key):
"""salsa20 (8 rounds) decryption."""
s20 = Salsa20(key=encrypt_key, IV=b"\x00" * 8, rounds=8)
return s20.encryptBytes(ciphertext)
def _decrypt_regcode_by_userid(reg_code: bytes, userid: bytes) -> bytes:
userid_digest = ripemd128(userid)
s20 = Salsa20(key=userid_digest, IV=b"\x00" * 8, rounds=8)
return s20.encryptBytes(reg_code)
class MDict:
"""
Base class which reads in header and key block.
It has no public methods and serves only as code sharing base class.
"""
def __init__(
self,
fname: str,
encoding: str = "",
passcode: "tuple[bytes, bytes] | None" = None,
) -> None:
self._fname = fname
self._encoding = encoding.upper()
self._encrypted_key = None
self._passcode = passcode
self.header = self._read_header()
# decrypt regcode to get the encrypted key
if passcode is not None:
regcode, userid = passcode
if isinstance(userid, str):
userid = userid.encode("utf8")
self._encrypted_key = _decrypt_regcode_by_userid(regcode, userid)
# MDict 3.0 encryption key derives from UUID if present
elif self._version >= 3.0:
uuid = self.header.get(b"UUID")
if uuid:
if xxhash is None:
raise RuntimeError(
"xxhash module is needed to read MDict 3.0 format"
"\n"
"Run `pip3 install xxhash` to install",
)
mid = (len(uuid) + 1) // 2
self._encrypted_key = xxhash.xxh64_digest(
uuid[:mid],
) + xxhash.xxh64_digest(uuid[mid:])
#self._key_list = self._read_keys()
def __repr__(self):
return (
f"MDict({self._fname!r}, "
f"encoding={self._encoding!r}, "
f"passcode={self._passcode})"
)
@property
def filename(self):
return self._fname
def __len__(self):
return self._num_entries
def __iter__(self):
return self.keys()
def keys(self):
"""Return an iterator over dictionary keys."""
return (key_value for key_id, key_value in self._key_list)
def _read_number(self, f):
return unpack(self._number_format, f.read(self._number_width))[0]
@staticmethod
def _read_int32(f):
return unpack(">I", f.read(4))[0]
@staticmethod
def _parse_header(header):
"""Extract attributes from <Dict attr="value" ... >."""
return {
key: _unescape_entities(value)
for key, value in re.findall(rb'(\w+)="(.*?)"', header, re.DOTALL)
}
def _decode_block(self, block, decompressed_size):
# block info: compression, encryption
info = unpack("<L", block[:4])[0]
compression_method = info & 0xF
encryption_method = (info >> 4) & 0xF
encryption_size = (info >> 8) & 0xFF
# adler checksum of the block data used as the encryption key if none given
adler32 = unpack(">I", block[4:8])[0]
encrypted_key = self._encrypted_key
if encrypted_key is None:
encrypted_key = ripemd128(block[4:8])
# block data
data = block[8:]
# decrypt
if encryption_method == 0:
decrypted_block = data
elif encryption_method == 1:
decrypted_block = (
_fast_decrypt(data[:encryption_size], encrypted_key)
+ data[encryption_size:]
)
elif encryption_method == 2:
decrypted_block = (
_salsa_decrypt(data[:encryption_size], encrypted_key)
+ data[encryption_size:]
)
else:
raise ValueError(f"encryption method {encryption_method} not supported")
# check adler checksum over decrypted data
if self._version >= 3:
assert hex(adler32) == hex(zlib.adler32(decrypted_block) & 0xFFFFFFFF)
# decompress
if compression_method == 0:
decompressed_block = decrypted_block
elif compression_method == 1:
header = b"\xf0" + pack(">I", decompressed_size)
decompressed_block = lzo.decompress(header + decrypted_block)
elif compression_method == 2:
decompressed_block = zlib.decompress(decrypted_block)
else:
raise ValueError(f"compression method {compression_method} not supported")
# check adler checksum over decompressed data
if self._version < 3:
assert hex(adler32) == hex(zlib.adler32(decompressed_block) & 0xFFFFFFFF)
return decompressed_block
def _decode_key_block_info(self, key_block_info_compressed):
if self._version >= 2:
# zlib compression
assert key_block_info_compressed[:4] == b"\x02\x00\x00\x00"
# decrypt if needed
if self._encrypt & 0x02:
key = ripemd128(key_block_info_compressed[4:8] + pack(b"<L", 0x3695))
key_block_info_compressed = key_block_info_compressed[
:8
] + _fast_decrypt(key_block_info_compressed[8:], key)
# decompress
key_block_info = zlib.decompress(key_block_info_compressed[8:])
# adler checksum
adler32 = unpack(">I", key_block_info_compressed[4:8])[0]
assert adler32 == zlib.adler32(key_block_info) & 0xFFFFFFFF
else:
# no compression
key_block_info = key_block_info_compressed
# decode
key_block_info_list = []
num_entries = 0
i = 0
if self._version >= 2:
byte_format = ">H"
byte_width = 2
text_term = 1
else:
byte_format = ">B"
byte_width = 1
text_term = 0
while i < len(key_block_info):
# number of entries in current key block
num_entries += unpack(
self._number_format,
key_block_info[i : i + self._number_width],
)[0]
i += self._number_width
# text head size
text_head_size = unpack(byte_format, key_block_info[i : i + byte_width])[0]
i += byte_width
# text head
if self._encoding != "UTF-16":
i += text_head_size + text_term
else:
i += (text_head_size + text_term) * 2
# text tail size
text_tail_size = unpack(byte_format, key_block_info[i : i + byte_width])[0]
i += byte_width
# text tail
if self._encoding != "UTF-16":
i += text_tail_size + text_term
else:
i += (text_tail_size + text_term) * 2
# key block compressed size
key_block_compressed_size = unpack(
self._number_format,
key_block_info[i : i + self._number_width],
)[0]
i += self._number_width
# key block decompressed size
key_block_decompressed_size = unpack(
self._number_format,
key_block_info[i : i + self._number_width],
)[0]
i += self._number_width
key_block_info_list.append(
(key_block_compressed_size, key_block_decompressed_size),
)
# assert num_entries == self._num_entries
return key_block_info_list
def _decode_key_block(self, key_block_compressed, key_block_info_list):
key_list = []
i = 0
for compressed_size, decompressed_size in key_block_info_list:
key_block = self._decode_block(
key_block_compressed[i : i + compressed_size],
decompressed_size,
)
# extract one single key block into a key list
key_list += self._split_key_block(key_block)
i += compressed_size
return key_list
def _split_key_block(self, key_block):
key_list = []
key_start_index = 0
while key_start_index < len(key_block):
# the corresponding record's offset in record block
key_id = unpack(
self._number_format,
key_block[key_start_index : key_start_index + self._number_width],
)[0]
# key text ends with '\x00'
if self._encoding == "UTF-16":
delimiter = b"\x00\x00"
width = 2
else:
delimiter = b"\x00"
width = 1
i = key_start_index + self._number_width
key_end_index = None
while i < len(key_block):
if key_block[i : i + width] == delimiter:
key_end_index = i
break
i += width
assert key_end_index is not None
key_text = (
key_block[key_start_index + self._number_width : key_end_index]
.decode(self._encoding, errors="ignore")
.strip()
)
key_start_index = key_end_index + width
key_list += [(key_id, key_text)]
return key_list
def _read_header(self):
f = open(self._fname, "rb")
# number of bytes of header text
header_bytes_size = unpack(">I", f.read(4))[0]
header_bytes = f.read(header_bytes_size)
# 4 bytes: adler32 checksum of header, in little endian
adler32 = unpack("<I", f.read(4))[0]
assert adler32 == zlib.adler32(header_bytes) & 0xFFFFFFFF
# mark down key block offset
self._key_block_offset = f.tell()
f.close()
# header text in utf-16 encoding ending with '\x00\x00'
if header_bytes[-2:] == b"\x00\x00":
header_text = header_bytes[:-2].decode("utf-16").encode("utf-8")
else:
header_text = header_bytes[:-1]
header_tag = self._parse_header(header_text)
if not self._encoding:
encoding = header_tag.get(b"Encoding", b"utf-8")
if sys.hexversion >= 0x03000000:
encoding = encoding.decode("utf-8")
# GB18030 > GBK > GB2312
if encoding in {"GBK", "GB2312"}:
encoding = "GB18030"
self._encoding = encoding
# encryption flag
# 0x00 - no encryption, "Allow export to text" is checked in MdxBuilder 3.
# 0x01 - encrypt record block, "Encryption Key" is given in MdxBuilder 3.
# 0x02 - encrypt key info block,
# "Allow export to text" is unchecked in MdxBuilder 3.
if b"Encrypted" not in header_tag or header_tag[b"Encrypted"] == b"No":
self._encrypt = 0
elif header_tag[b"Encrypted"] == b"Yes":
self._encrypt = 1
else:
self._encrypt = int(header_tag[b"Encrypted"])
# stylesheet attribute if present takes form of:
# style_number # 1-255
# style_begin # or ''
# style_end # or ''
# store stylesheet in dict in the form of
# {'number' : ('style_begin', 'style_end')}
self._stylesheet = {}
if header_tag.get(b"StyleSheet"):
lines = header_tag[b"StyleSheet"].decode('utf8',errors='ignore').splitlines()
self._stylesheet = {
lines[i]: (lines[i + 1], lines[i + 2]) for i in range(0, len(lines), 3)
}
if b"Title" in header_tag:
self._title = header_tag[b"Title"].decode("utf-8",errors='ignore')
else:
self._title = ""
# before version 2.0, number is 4 bytes integer
# version 2.0 and above uses 8 bytes
self._version = float(header_tag[b"GeneratedByEngineVersion"])
if self._version < 2.0:
self._number_width = 4
self._number_format = ">I"
else:
self._number_width = 8
self._number_format = ">Q"
# version 3.0 uses UTF-8 only
if self._version >= 3:
self._encoding = "UTF-8"
return header_tag
def _read_keys(self):
if self._version >= 3:
return self._read_keys_v3()
# if no regcode is given, try brute-force (only for engine <= 2)
if (self._encrypt & 0x01) and self._encrypted_key is None:
log.warning("Trying brute-force on encrypted key blocks")
return self._read_keys_brutal()
return self._read_keys_v1v2()
def _read_keys_v3(self):
f = open(self._fname, "rb")
f.seek(self._key_block_offset)
# find all blocks offset
while True:
block_type = self._read_int32(f)
block_size = self._read_number(f)
block_offset = f.tell()
# record data
if block_type == 0x01000000:
self._record_block_offset = block_offset
# record index
elif block_type == 0x02000000:
self._record_index_offset = block_offset
# key data
elif block_type == 0x03000000:
self._key_data_offset = block_offset
# key index
elif block_type == 0x04000000:
self._key_index_offset = block_offset
else:
raise RuntimeError(f"Unknown block type {block_type}")
f.seek(block_size, 1)
# test the end of file
if f.read(4):
f.seek(-4, 1)
else:
break
# read key data
f.seek(self._key_data_offset)
number = self._read_int32(f)
self._read_number(f) # total_size
key_list = []
for _ in range(number):
decompressed_size = self._read_int32(f)
compressed_size = self._read_int32(f)
block_data = f.read(compressed_size)
decompressed_block_data = self._decode_block(block_data, decompressed_size)
key_list.extend(self._split_key_block(decompressed_block_data))
f.close()
self._num_entries = len(key_list)
return key_list
def _read_keys_v1v2(self):
f = open(self._fname, "rb")
f.seek(self._key_block_offset)
# the following numbers could be encrypted
num_bytes = 8 * 5 if self._version >= 2.0 else 4 * 4
block = f.read(num_bytes)
if self._encrypt & 1:
block = _salsa_decrypt(block, self._encrypted_key)
# decode this block
sf = BytesIO(block)
# number of key blocks
num_key_blocks = self._read_number(sf)
# number of entries
self._num_entries = self._read_number(sf)
# number of bytes of key block info after decompression
if self._version >= 2.0:
self._read_number(sf) # key_block_info_decomp_size
# number of bytes of key block info
key_block_info_size = self._read_number(sf)
# number of bytes of key block
key_block_size = self._read_number(sf)
# 4 bytes: adler checksum of previous 5 numbers
if self._version >= 2.0:
adler32 = unpack(">I", f.read(4))[0]
assert adler32 == (zlib.adler32(block) & 0xFFFFFFFF)
# read key block info, which indicates key block's compressed
# and decompressed size
key_block_info = f.read(key_block_info_size)
key_block_info_list = self._decode_key_block_info(key_block_info)
assert num_key_blocks == len(key_block_info_list)
# read key block
key_block_compressed = f.read(key_block_size)
# extract key block
key_list = self._decode_key_block(key_block_compressed, key_block_info_list)
self._record_block_offset = f.tell()
f.close()
return key_list
def _read_keys_brutal(self):
f = open(self._fname, "rb")
f.seek(self._key_block_offset)
# the following numbers could be encrypted, disregard them!
if self._version >= 2.0:
num_bytes = 8 * 5 + 4
key_block_type = b"\x02\x00\x00\x00"
else:
num_bytes = 4 * 4
key_block_type = b"\x01\x00\x00\x00"
f.read(num_bytes) # block
# key block info
# 4 bytes '\x02\x00\x00\x00'
# 4 bytes adler32 checksum
# unknown number of bytes follows until '\x02\x00\x00\x00'
# which marks the beginning of key block
key_block_info = f.read(8)
if self._version >= 2.0:
assert key_block_info[:4] == b"\x02\x00\x00\x00"
while True:
fpos = f.tell()
t = f.read(1024)
index = t.find(key_block_type)
if index != -1:
key_block_info += t[:index]
f.seek(fpos + index)
break
key_block_info += t
key_block_info_list = self._decode_key_block_info(key_block_info)
key_block_size = sum(list(zip(*key_block_info_list, strict=False))[0])
# read key block
key_block_compressed = f.read(key_block_size)
# extract key block
key_list = self._decode_key_block(key_block_compressed, key_block_info_list)
self._record_block_offset = f.tell()
f.close()
self._num_entries = len(key_list)
return key_list
def items(self):
"""
Return a generator which in turn produce tuples in the
form of (filename, content).
"""
return self._read_records()
def _read_records(self):
if self._version >= 3:
yield from self._read_records_v3()
else:
yield from self._read_records_v1v2()
def _read_records_v3(self):
f = open(self._fname, "rb")
f.seek(self._record_block_offset)
offset = 0
i = 0
size_counter = 0
num_record_blocks = self._read_int32(f)
self._read_number(f) # num_bytes
for _ in range(num_record_blocks):
file_pos=f.tell()
decompressed_size = self._read_int32(f)
compressed_size = self._read_int32(f)
record_block = self._decode_block(
f.read(compressed_size),
decompressed_size,
)
# split record block according to the offset info from key block
while i < len(self._key_list):
record_start, key_text = self._key_list[i]
# reach the end of current record block
if record_start - offset >= len(record_block):
break
# record end index
if i < len(self._key_list) - 1:
record_end = self._key_list[i + 1][0]
else:
record_end = len(record_block) + offset
i += 1
yield dict(key_text= key_text, file_pos= file_pos, decompressed_size= len(record_block),record_start=record_start, offset=offset, record_end=record_end,compressed_size=compressed_size)
offset += len(record_block)
size_counter += compressed_size
def _read_records_v1v2(self):
f = open(self._fname, "rb")
f.seek(self._record_block_offset)
num_record_blocks = self._read_number(f)
num_entries = self._read_number(f)
assert num_entries == self._num_entries
record_block_info_size = self._read_number(f)
self._read_number(f) # record_block_size
# record block info section
record_block_info_list = []
size_counter = 0
for _ in range(num_record_blocks):
compressed_size = self._read_number(f)
decompressed_size = self._read_number(f)
record_block_info_list += [(compressed_size, decompressed_size)]
size_counter += self._number_width * 2
assert size_counter == record_block_info_size
# actual record block
offset = 0
i = 0
size_counter = 0
for compressed_size, decompressed_size in record_block_info_list:
file_pos=f.tell()
record_block_compressed = f.read(compressed_size)
try:
record_block = self._decode_block(
record_block_compressed,
decompressed_size,
)
except zlib.error:
log.error("zlib decompress error")
log.debug(f"record_block_compressed = {record_block_compressed!r}")
continue
# split record block according to the offset info from key block
while i < len(self._key_list):
record_start, key_text = self._key_list[i]
# reach the end of current record block
if record_start - offset >= len(record_block):
break
# record end index
if i < len(self._key_list) - 1:
record_end = self._key_list[i + 1][0]
else:
record_end = len(record_block) + offset
i += 1
yield dict(key_text= key_text, file_pos= file_pos, decompressed_size= len(record_block),record_start=record_start, offset=offset, record_end=record_end,compressed_size=compressed_size)
offset += len(record_block)
size_counter += compressed_size
# assert size_counter == record_block_size
f.close()
def read_records(self, index):
f = open(self._fname, "rb")
f.seek(index["file_pos"])
record_block_compressed = f.read(index["compressed_size"])
f.close()
decompressed_size = index["decompressed_size"]
try:
record_block = self._decode_block(
record_block_compressed,
decompressed_size,
)
except zlib.error:
log.error("zlib decompress error")
log.debug(f"record_block_compressed = {record_block_compressed!r}")
# split record block according to the offset info from key block
data = record_block[
index["record_start"]
- index["offset"] : index["record_end"]
- index["offset"]
]
return data
# assert size_counter == record_block_size
class MDD(MDict):
"""
MDict resource file format (*.MDD) reader.
>>> mdd = MDD("example.mdd")
>>> len(mdd)
208
>>> for filename,content in mdd.items():
... print(filename, content[:10])
"""
def __init__(
self,
fname: str,
passcode: "tuple[bytes, bytes] | None" = None,
) -> None:
MDict.__init__(self, fname, encoding="UTF-16", passcode=passcode)
class MDX(MDict):
"""
MDict dictionary file format (*.MDD) reader.
>>> mdx = MDX("example.mdx")
>>> len(mdx)
42481
>>> for key,value in mdx.items():
... print(key, value[:10])
"""
def __init__(
self,
fname: str,
encoding: str = "",
substyle: bool = False,
passcode: "tuple[bytes, bytes] | None" = None,
) -> None:
MDict.__init__(self, fname, encoding, passcode)
self._substyle = substyle
def _substitute_stylesheet(self, txt):
# substitute stylesheet definition
txt_list = re.split(r"`\d+`", txt)
txt_tag = re.findall(r"`\d+`", txt)
txt_styled = txt_list[0]
for j, p in enumerate(txt_list[1:]):
key = txt_tag[j][1:-1]
try:
style = self._stylesheet[key]
except KeyError:
log.error(f'invalid stylesheet key "{key}"')
continue
if p and p[-1] == "\n":
txt_styled = txt_styled + style[0] + p.rstrip() + style[1] + "\r\n"
else:
txt_styled = txt_styled + style[0] + p + style[1]
return txt_styled
def _treat_record_data(self, data):
# convert to utf-8
data = (
data.decode(self._encoding, errors="ignore").strip("\x00")
)
# substitute styles
if self._substyle and self._stylesheet:
data = self._substitute_stylesheet(data)
return data # noqa: RET504

View File

@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
# mypy: ignore-errors
#
# Copyright (C) 2016-2023 Saeed Rasooli on https://github.com/ilius/pyglossary/
# Copyright (C) 2015 Z. H. Liu on https://github.com/zhansliu/writemdict
#
# ripemd128.py - A simple ripemd128 library in pure Python.
#
# Supports both Python 2 (versions >= 2.6) and Python 3.
#
# Usage:
# from ripemd128 import ripemd128
# digest = ripemd128(b"The quick brown fox jumps over the lazy dog")
# assert(
# digest == b"\x3f\xa9\xb5\x7f\x05\x3c\x05\x3f\xbe\x27\x35\xb2\x38\x0d\xb5\x96"
# )
import struct
__all__ = ["ripemd128"]
# follows this description: http://homes.esat.kuleuven.be/~bosselae/ripemd/rmd128.txt
def f(j, x, y, z):
assert 0 <= j < 64
if j < 16:
return x ^ y ^ z
if j < 32:
return (x & y) | (z & ~x)
if j < 48:
return (x | (0xFFFFFFFF & ~y)) ^ z
return (x & z) | (y & ~z)
def K(j):
assert 0 <= j < 64
if j < 16:
return 0x00000000
if j < 32:
return 0x5A827999
if j < 48:
return 0x6ED9EBA1
return 0x8F1BBCDC
def Kp(j):
assert 0 <= j < 64
if j < 16:
return 0x50A28BE6
if j < 32:
return 0x5C4DD124
if j < 48:
return 0x6D703EF3
return 0x00000000
def padandsplit(message: bytes):
"""
returns a two-dimensional array X[i][j] of 32-bit integers, where j ranges
from 0 to 16.
First pads the message to length in bytes is congruent to 56 (mod 64),
by first adding a byte 0x80, and then padding with 0x00 bytes until the
message length is congruent to 56 (mod 64). Then adds the little-endian
64-bit representation of the original length. Finally, splits the result
up into 64-byte blocks, which are further parsed as 32-bit integers.
"""
origlen = len(message)
padlength = 64 - ((origlen - 56) % 64) # minimum padding is 1!
message += b"\x80"
message += b"\x00" * (padlength - 1)
message += struct.pack("<Q", origlen * 8)
assert len(message) % 64 == 0
return [
[struct.unpack("<L", message[i + j : i + j + 4])[0] for j in range(0, 64, 4)]
for i in range(0, len(message), 64)
]
def add(*args):
return sum(args) & 0xFFFFFFFF
def rol(s, x):
assert s < 32
return (x << s | x >> (32 - s)) & 0xFFFFFFFF
r = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
]
rp = [
5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
]
s = [
11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
]
sp = [
8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
]
def ripemd128(message: bytes) -> bytes:
h0 = 0x67452301
h1 = 0xEFCDAB89
h2 = 0x98BADCFE
h3 = 0x10325476
X = padandsplit(message)
for Xi in X:
A, B, C, D = h0, h1, h2, h3
Ap, Bp, Cp, Dp = h0, h1, h2, h3
for j in range(64):
T = rol(
s[j],
add(
A,
f(j, B, C, D),
Xi[r[j]],
K(j),
),
)
A, D, C, B = D, C, B, T
T = rol(
sp[j],
add(
Ap,
f(63 - j, Bp, Cp, Dp),
Xi[rp[j]],
Kp(j),
),
)
Ap, Dp, Cp, Bp = Dp, Cp, Bp, T
T = add(h1, C, Dp)
h1 = add(h2, D, Ap)
h2 = add(h3, A, Bp)
h3 = add(h0, B, Cp)
h0 = T
return struct.pack("<LLLL", h0, h1, h2, h3)
def hexstr(bstr):
return "".join(f"{b:02x}" for b in bstr)

View File

@ -39,12 +39,12 @@
.tab-widget .tab-button {
padding: 5px 20px;
padding: 7px 15px;
background-color: #cccccccc;
border: none;
cursor: pointer;
display: inline-block;
line-height: 25px;
line-height: 20px;
}
.tab-widget .tab-button.active {