Source code for p115pickcode

#!/usr/bin/env python3
# encoding: utf-8

__author__ = "ChenyangGao <https://chenyanggao.github.io>"
__version__ = (0, 0, 5)
__all__ = [
    "ALPHABET", "PREFIX_TO_TRANSTAB", "PREFIX_TO_TRANSTAB_REV", 
    "PREFIX_TO_FIRST_SUFFIX", "FIRST_SUFFIX_TO_PREFIX", 
    "get_stable_point", "is_valid_pickcode", "pickcode_to_id", 
    "id_to_pickcode", "to_id", "to_pickcode", 
]
__license__ = "GPLv3 <https://www.gnu.org/licenses/gpl-3.0.txt>"

from string import ascii_lowercase, digits
from typing import Final, Literal


#: 字符表,任何一个 pickcode 只包含这 36 个字符
ALPHABET: Final = digits + ascii_lowercase
#: pickcode 的前缀对应的替换表,用来把明文转换为密文
PREFIX_TO_TRANSTAB: Final = {
    "a": str.maketrans("fuln1ytpj3smg8d5a094qh7cxkbi62zvewro", ALPHABET), 
    "b": str.maketrans("sk721n9a0emlfpcrzbqdw3gjh6ty5xui48vo", ALPHABET), 
    "c": str.maketrans("ywcz3hite6f1j0guoakvdb2ns7p8qr9ml5x4", ALPHABET), 
    "d": str.maketrans("rq2vl5o7wsken9u8tp4jg3zbyc6xmhifd01a", ALPHABET), 
    "e": str.maketrans("ljm9eqbcfhw7ktv3x1dgp5ua8y6s4znr2io0", ALPHABET), 
    "fa": str.maketrans("fumk0ytpj3sng8d5a194qh7cxlbi62zvewro", ALPHABET), 
    "fb": str.maketrans("sk732o9a1enmfpcrzbqdw4gjh6ty5xui08vl", ALPHABET), 
    "fc": str.maketrans("ywcz6hite9f4j3gup2kvdb5osal0qr1nm8x7", ALPHABET), 
    "fd": str.maketrans("on6vl0r2wpkeq9u3ts8jg7zbyc1xmhifd45a", ALPHABET), 
    "fe": str.maketrans("ljm0es2cfhwakqv6x4dgp8r1by9u7znt5io3", ALPHABET), 
}
#: pickcode 的前缀对应的替换表,用来把密文转换为明文
PREFIX_TO_TRANSTAB_REV: Final = {k: {v: k for k, v in v.items()} for k, v in PREFIX_TO_TRANSTAB.items()}
#: pickcode 的前缀(如果首字母是 "f",则是 pickcode[:2],否则是 pickcode[0])和后缀首字符 pickcode[-4] 的对应关系(之所以可以如此,因为不动点左起第 1 个字符固定是 0)
PREFIX_TO_FIRST_SUFFIX: Final = {a: chr(b[ord("0")]) for a, b in PREFIX_TO_TRANSTAB.items()}
#: pickcode 的后缀首字符 pickcode[-4] 和前缀(如果首字母是 "f",则是 pickcode[:2],否则是 pickcode[0])的对应关系
FIRST_SUFFIX_TO_PREFIX: Final = {v: k for k, v in PREFIX_TO_FIRST_SUFFIX.items()}


def b36encode(n: int, /) -> str:
    "数字转换为 36 进制字符串"
    if n < 0:
        return "-" + b36encode(-n)
    elif n < 36:
        return ALPHABET[n]
    chars: list[str] = []
    add_char = chars.append
    while n:
        n, r = divmod(n, 36)
        add_char(ALPHABET[r])
    return "".join(reversed(chars))


def b36decode(s: str, /) -> int:
    "36 进制字符串转换为数字"
    return int(s, 36)


def get_stable_point(pickcode: str, /) -> str:
    """获取 pickcode 所对应的不动点

    .. node::
        同一个用户,它的网盘中的所有文件或目录的 pickcode,从这个函数得到的结果都相同

    :param pickcode: 提取码、不动点或者加密后的不动点

    :return: 不动点,长度为 4,范围在 0-9 和 a-z 内的字符串,左起第 1 个字符是 0
    """
    len_pickcode = len(pickcode)
    if len_pickcode < 4:
        return "0" * (4 - len_pickcode) + pickcode
    elif len_pickcode >= 6 and pickcode[:2] in ("fa", "fb", "fc", "fd", "fe"):
        prefix = pickcode[:2]
    elif len_pickcode >= 5 and pickcode[0] in ("a", "b", "c", "d", "e"):
        prefix = pickcode[0]
    elif pickcode[-4] == "0":
        return pickcode[-4:]
    else:
        prefix = FIRST_SUFFIX_TO_PREFIX[pickcode[-4]]
    transtab = PREFIX_TO_TRANSTAB_REV[prefix]
    return pickcode[-4:].translate(transtab)


[docs] def is_valid_pickcode(pickcode: str, /) -> bool: """是否合法的 pickcode :param pickcode: 提取码 :return: 是否合法 """ if not pickcode: return True elif pickcode.strip(ALPHABET): return False len_pickcode = len(pickcode) if pickcode.startswith("f") and len_pickcode < 7 or len_pickcode < 6: return False prefix = pickcode[:2] if pickcode.startswith("f") else pickcode[0] return prefix in PREFIX_TO_TRANSTAB and prefix == FIRST_SUFFIX_TO_PREFIX[pickcode[-4]]
def pickcode_to_id(pickcode: str, /) -> int: """从 115 的 pickcode 得到 id .. note:: 0. 规定根目录 id 为 0 所对应的提取码为空字符串 "" 1. 提取码中只含有 0-9 和 a-z 这 36 个字符 2. 提取码由 3 部分组成 `f"{前缀}{中缀}{后缀}"`,而提取码的前缀唯一确定了简单替换加密所用的替换表: - 前缀:如果首字母是 "f",则是 `pickcode[:2]`,否则是 `pickcode[0]` - 后缀:最后 4 个字符 `pickcode[-4:]`,同一个用户,由前缀所对应的的替换表进行解密后可得一个固定值,称为不动点(它的第 1 个字符是 "0") - 中缀:除去前缀和后缀剩余的部分,解密后可以得到 id 的 36 进制表示 3. 如果 `pickcode` 的首字母是 "f",暗示这是个目录,允许的形式有 5 种,都包含前后缀和一个经过加密的 id: - f"fa{加密id}4{剩余3位后缀}" - f"fb{加密id}w{剩余3位后缀}" - f"fc{加密id}r{剩余3位后缀}" - f"fd{加密id}5{剩余3位后缀}" - f"fe{加密id}3{剩余3位后缀}" 4. 如果 `pickcode` 的首字母是 "a"、"b"、"c"、"d"、"e" 之一,暗示这是个文件,允许的结构有 5 种,都包含前后缀和一个经过加密的 id: - f"a{加密id}h{剩余3位后缀}" - f"b{加密id}8{剩余3位后缀}" - f"c{加密id}d{剩余3位后缀}" - f"d{加密id}x{剩余3位后缀}" - f"e{加密id}z{剩余3位后缀}" 5. 前缀和后缀的第 1 个字符是一一对应的,可以互相推导。同一个用户的不动点固定,但不同用户不动点往往不同。 :param pickcode: 提取码 :return: id """ if not pickcode: return 0 if pickcode.startswith("f"): prefix = pickcode[:2] cipher = pickcode[2:-4] else: prefix = pickcode[0] cipher = pickcode[1:-4] transtab = PREFIX_TO_TRANSTAB_REV[prefix] return b36decode(cipher.translate(transtab)) def id_to_pickcode( id: int, /, stable_point: str = "0000", prefix: Literal["", "a", "b", "c", "d", "e", "fa", "fb", "fc", "fd", "fe"] = "a", ) -> str: """从 115 的 id 得到 pickcode :param id: 文件或目录的 id :param stable_point: 不动点(长度为 4,范围在 0-9 和 a-z 内的字符串,左起第 1 个字符是 0),或者是 `pickcode` 或者 `pickcode[-4:]` :param prefix: 前缀,如果为 "",则自动确定,确定不了时默认为 "a" :return: 提取码 """ if not id: return "" elif id < 0: raise ValueError(f"negtive id is not allowed, got {id!r}") len_stable_point = len(stable_point) prefix_: str = prefix or "a" is_stable_point = True if len_stable_point < 4: stable_point = "0" * (4 - len_stable_point) + stable_point elif len_stable_point >= 6 and stable_point[:2] in ("fa", "fb", "fc", "fd", "fe"): prefix_ = stable_point[:2] is_stable_point = False elif len_stable_point >= 5 and stable_point[0] in ("a", "b", "c", "d", "e"): prefix_ = stable_point[0] is_stable_point = False elif stable_point[-4] != "0": prefix_ = FIRST_SUFFIX_TO_PREFIX[stable_point[-4]] is_stable_point = False if len_stable_point > 4: stable_point = stable_point[-4:] transtab = PREFIX_TO_TRANSTAB[prefix or prefix_] if is_stable_point: suffix = stable_point.translate(transtab) elif not prefix or prefix == prefix_: suffix = stable_point prefix_ = prefix or prefix_ else: stable_point = stable_point.translate(PREFIX_TO_TRANSTAB_REV[prefix_]) suffix = stable_point.translate(transtab) prefix_ = prefix cipher = b36encode(id).translate(transtab) return prefix_ + cipher + suffix def to_id(pickcode: int | str, /) -> int: """把可能是 id 或 pickcode 的一律转换成 id .. note:: 规定:根目录 id 为 0 对应的提取码是 "" :param pickcode: 可能是 pickcode 或 id :return: id """ if isinstance(pickcode, int): id = pickcode if id < 0: raise ValueError(f"negtive id is not allowed, got {id!r}") return id if not pickcode: return 0 elif pickcode.startswith(("a", "b", "c", "d", "e", "f")): return pickcode_to_id(pickcode) else: return int(pickcode) def to_pickcode( id: int | str, /, stable_point: str = "0000", prefix: Literal["", "a", "b", "c", "d", "e", "fa", "fb", "fc", "fd", "fe"] = "a", ) -> str: """把可能是 id 或 pickcode 的一律转换成 pickcode .. note:: 规定:空提取码 "" 对应的 id 是 0 :param id: 可能是 id 或 pickcode :param stable_point: 不动点(长度为 4,范围在 0-9 和 a-z 内的字符串,左起第 1 个字符是 0),或者是 `pickcode` 或者 `pickcode[-4:]` :param prefix: 前缀,如果为 "",则自动确定,确定不了时默认为 "a" :return: pickcode """ if not id: return "" elif isinstance(id, str): if id.startswith(("a", "b", "c", "d", "e", "f")): return id id = int(id) return id_to_pickcode(id, stable_point, prefix=prefix)