{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "46c5ef50",
   "metadata": {},
   "source": [
    "# anki sync文档\n",
    "## 数据包\n",
    "### http\n",
    "```python\n",
    "same = {\n",
    "    'SERVER_PROTOCOL': 'HTTP/1.0',  'REQUEST_METHOD': 'POST', 'PATH_INFO': '/msync/begin', 'QUERY_STRING': '', \n",
    "}\n",
    "```\n",
    "### android 包\n",
    "```python\n",
    "env = { 'CONTENT_LENGTH': '449','CONTENT_TYPE': 'multipart/form-data; boundary=Anki-sync-boundary', 'HTTP_ACCEPT_ENCODING': 'gzip', 'HTTP_USER_AGENT': 'AnkiDroid-2.15.6'}\n",
    "```\n",
    "### pc包\n",
    "```python\n",
    "b'{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n",
    "```\n",
    "```python\n",
    "env = { 'CONTENT_LENGTH': '46', 'CONTENT_TYPE': 'application/octet-stream',  'HTTP_ANKI_SYNC': '{\"v\":11,\"k\":\"a8b327a1e7ccbf704c4a60a23a8c4a57\",\"c\":\"2.1.64,581f82c5,windows\",\"s\":\"cJcynh\"}' }\n",
    "```\n",
    "\n",
    "## PC数据\n",
    "### 数据协议\n",
    "- 输入\n",
    "    - path is ::/msync/begin body is::b'(\\xb5/\\xfd\\x00X)\\x01\\x00{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n",
    "    - path is ::/sync/meta body is::b'(\\xb5/\\xfd\\x00Xi\\x01\\x00{\"v\":11,\"cv\":\"anki,2.1.64 (581f82c5),win:10\"}'\n",
    "    - path is ::/sync/hostKey body is::b'(\\xb5/\\xfd\\x00X\\xc9\\x00\\x00{\"u\":\"anki\",\"p\":\"ouczbs\"}'\n",
    "- 输出\n",
    "    - 不加密,不压缩\n",
    "### 流式解析\n",
    "- 在数据较大时,使用结果不太对\n",
    "```python\n",
    "import pyzstd\n",
    "data = b'{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n",
    "encoder = pyzstd.ZstdCompressor()\n",
    "encoder.compress(data)\n",
    "body = encoder.flush()\n",
    "print(body)\n",
    "decoder = pyzstd.ZstdDecompressor()\n",
    "data = decoder.decompress(body)\n",
    "print(data)\n",
    "```\n",
    "### 快速解析\n",
    "```python\n",
    "import pyzstd\n",
    "data = b'(\\xb5/\\xfd\\x00X)\\x01\\x00{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n",
    "# with open(\"conf/data.txt\",\"wb\")as f:\n",
    "#     f.write(data)\n",
    "with open(\"data.txt\",\"rb\")as f:\n",
    "    data = f.read()\n",
    "print(len(data))\n",
    "body = pyzstd.compress(data)\n",
    "print(len(body))\n",
    "data = pyzstd.decompress(body)\n",
    "print(len(data))\n",
    "```\n",
    "## Android 数据\n",
    "### 数据协议\n",
    "- 输入\n",
    "```python\n",
    "data = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"k\"\\r\\n\\r\\n0361e82baf7e86bf1b5f4339382fccd4\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"s\"\\r\\n\\r\\na87d9d0a\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"c\"\\r\\n\\r\\n1\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"data\"; filename=\"data\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n\\x1f\\x8b\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00e\\x91MN\\xc30\\x10\\x85\\xafb\\xcd\\x86\\x8d\\xd5$(M\\x9b\\xa8t\\xc3\\x16!\\x84\\xd8U]\\xb8\\xf1\\xa4\\xb1\\xea\\xd8\\x95\\xed6TUwH\\xdc\\xa0\\x1b\\x16p\\x07\\x84\\xc4\\x81\\x10\\xa2\\xb7\\xc0I\\xf9+\\xdd\\xcd|\\xf3\\xe4yo\\xbc\\x86\\xbcdj\\x8a\\x16\\xb25T\\x9a\\xa3\\xf4\\xd5hL\\x81c>k\\xca\\xd1\\x1a\\x04\\x87,J\\xd2\\xb0\\x9b\\xc4q\\x14\\xa7Q\\x8f6\\xca\\x1f\\xd6Mc\\n\\x8aU\\x08\\x19|\\xbcl\\xdf^\\xb7\\xbb\\x87\\xbb\\xdd\\xe3\\xf3\\xfb\\xd3=PXX\\xe5\\x95\\x11\\x05i\\xd4\\x8d\\xe6l\\xe5\\xdf\\x8ch\\xe87\\x18\\\\\\x1e\\x02\\x85\\xf5!p\\xa2\\xc2_\\x92\\xf4{\\xa9\\x87\\xb9\\x96\\x92\\xcd-z\\x03\\x05\\x93\\x16)L\\x8c\\xae-\\x9a\\xf3\\xa3\\x01G\\x9b{SW\\x12\\x99Eb\\x11\\x89+\\x91\\x0c\\x18)\\r\\x16g\\'\\xa5ss\\x9b\\x05\\x01S3Q\\xe3\\xa4\\xa3\\xd0\\x05\\xb6d\\x06y T\\xa1\\x83^\\xdc\\x8d\\x92~\\x18\\x9f\\x9e\\x0c\\xf7\\x984G!s6\\xc5A\\xc0\\x86\\xa4\\xd0\\x86T\\xda i\\xd4\\x1d\\x9f\\x95\\xaf|\\xd6\\xb0M&\\xb0\\xbe\\x10\\x95p_\\xf6\\xd5B\\xca6\\xe01\\xfc#\\xfe\\xaf\\xfb\\xees\\xad\\n\\x7fD\\nx\\xebP\\xf1K\\xac\\xdb5\\xfb\\xee\\x1a\\x97\\xbe\\xdb\\x8c\\xe9h\\xdc\\x9c\\x8cM\\xdb\\x0f\\xdcl>\\x019A\\x97\\x92\\xda\\x01\\x00\\x00\\r\\n--Anki-sync-boundary--\\r\\n'\n",
    "\n",
    "data = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"k\"\\r\\n\\r\\nxQ0tcIOYlDyeJhZV\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"s\"\\r\\n\\r\\n8d6272d6\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"c\"\\r\\n\\r\\n1\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"data\"; filename=\"data\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n\\x1f\\x8b\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xabV*S\\xb224\\xd0QJ\\x06\\xd2J\\x89y\\xd9\\x99)E\\xf9\\x99):Fz\\x86\\xa6zf:\\x89y`\\xae\\x95\\xa1\\x91\\x95\\xaf\\x91\\x81\\x81\\xb9\\x97a\\xb0\\xb3R-\\x00\\x16\\x80\\xd5\\x825\\x00\\x00\\x00\\r\\n--Anki-sync-boundary--\\r\\n'\n",
    "```\n",
    "- 输出\n",
    "    - 不加密,zstd压缩\n",
    "### 正则匹配\n",
    "```python\n",
    "import re\n",
    "pattern = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"(.*?)\"\\\n",
    "(?:; filename=\".*?\"\\r\\nContent-Type: application/octet-stream)*\\r\\n\\r\\n(.*?|[\\s\\S]*?)\\r\\n(?:--Anki-sync-boundary--\\r\\n$)*'\n",
    "res = re.findall(pattern,data)\n",
    "print(res)\n",
    "```\n",
    "\n",
    "## python\n",
    "### api 版本号\n",
    "`sync.py >> SYNC_VER = 11`\n",
    "### 数据包\n",
    "```python\n",
    "#class Request\n",
    "def parseAndroid(self, body, env):\n",
    "    env[\"APP_CLIENT_TYPE\"] = ClientType.Android\n",
    "    request_items_dict = {}\n",
    "    pattern = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"(.*?)\"\\\n",
    "(?:; filename=\".*?\"\\r\\nContent-Type: application/octet-stream)*\\r\\n\\r\\n(.*?|[\\s\\S]*?)\\r\\n(?:--Anki-sync-boundary--\\r\\n$)*'\n",
    "    res = re.findall(pattern , body)\n",
    "    for k in res:\n",
    "        if len(k) < 2:\n",
    "            logger.error(f\"error pattern match: {k}\")\n",
    "            continue\n",
    "        v = k[1] if k[0] == b\"data\" else k[1].decode()\n",
    "        request_items_dict[k[0].decode()] = v\n",
    "    if \"data\" in request_items_dict and \"c\" in request_items_dict \\\n",
    "        and int(request_items_dict[\"c\"]):\n",
    "        data = request_items_dict[\"data\"] \n",
    "        with gzip.GzipFile(mode=\"rb\", fileobj=io.BytesIO(data)) as gz:\n",
    "            data = gz.read()\n",
    "        request_items_dict[\"data\"]  = data\n",
    "    if \"data\" not in request_items_dict:\n",
    "        #pdb.set_trace()\n",
    "        pass\n",
    "    return request_items_dict\n",
    "def parsePC(self, body, env):\n",
    "    env[\"APP_CLIENT_TYPE\"] = ClientType.PC\n",
    "    request_items_dict = {}\n",
    "    body = pyzstd.decompress(body)\n",
    "    request_items_dict[\"data\"] = body\n",
    "    http_anki_sync = env.get(\"HTTP_ANKI_SYNC\", \"\")\n",
    "    if http_anki_sync != \"\":\n",
    "        anki_sync = json.loads(http_anki_sync)\n",
    "        for k in anki_sync.keys():\n",
    "            request_items_dict[k] = anki_sync[k]\n",
    "    return request_items_dict\n",
    "    \n",
    "def parse(self):\n",
    "    if 'application/octet-stream' in env.get(\"CONTENT_TYPE\",\"\"):\n",
    "        request_items_dict = self.parsePC(body, env)\n",
    "    else:\n",
    "        request_items_dict = self.parseAndroid(body, env)\n",
    "def wrap_body(self, body, env):\n",
    "    if \"APP_CLIENT_TYPE\" not in env:\n",
    "        return body\n",
    "    if env[\"APP_CLIENT_TYPE\"] == ClientType.PC:\n",
    "        return pyzstd.compress(body)\n",
    "    # if env[\"APP_CLIENT_TYPE\"] == ClientType.Android:\n",
    "    #     return body\n",
    "    return body\n",
    "```\n",
    "\n",
    "### 注解\n",
    "```python\n",
    "import types\n",
    "from functools import wraps\n",
    "class chunked(object):\n",
    "    \"\"\"decorator\"\"\"\n",
    "    def __init__(self, func):\n",
    "        wraps(func)(self)\n",
    "        print(\"__init__ chunked\",func)\n",
    "\n",
    "    def __call__(self, *args, **kwargs):\n",
    "        clss = args[0]\n",
    "        environ = args[1]\n",
    "        start_response = args[2]\n",
    "        args = (\n",
    "            clss,\n",
    "            environ,\n",
    "        )\n",
    "        w = self.__wrapped__(*args, **kwargs)\n",
    "        print(\"__call__ chunked\",clss,environ,start_response,kwargs,w)\n",
    "        return \"4\"\n",
    "\n",
    "    def __get__(self, instance, cls):\n",
    "        print(\"__get__ chunked\", instance , cls)\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return types.MethodType(self, instance)\n",
    "class SyncApp:\n",
    "    def __init__(self, config):\n",
    "        print(\"__init__SyncApp\",config)\n",
    "\n",
    "    @chunked\n",
    "    def __call__(self, req):\n",
    "        print(\"__call__SyncApp\",req)\n",
    "        return \"3\"\n",
    "sync = SyncApp(\"config\")\n",
    "sync(1,2)\n",
    " ```\n",
    "### webob\n",
    "- app\n",
    "- environ\n",
    " - 环境变量\n",
    " - 请求数据\n",
    "  - body env.get(\"wsgi.input\")\n",
    "  - header env.get(\"HTTP_ANKI_SYNC\", \"\")\n",
    "- request\n",
    "- response\n",
    "\n",
    "## rust\n",
    "```rust\n",
    "pub fn decode_zstd_body_stream_for_client<S, E>(data: S) -> impl Stream<Item = HttpResult<Bytes>>\n",
    "where\n",
    "    S: Stream<Item = Result<Bytes, E>> + Unpin,\n",
    "    E: Display,\n",
    "{\n",
    "    let response_total = resp\n",
    "        .headers()\n",
    "        .get(&ORIGINAL_SIZE)\n",
    "        .and_then(|v| v.to_str().ok())\n",
    "        .and_then(|v| v.parse::<u32>().ok())\n",
    "        .or_bad_request(\"missing original size\")?;\n",
    "    stream.map(move |res| match res {\n",
    "        Ok(bytes) => {\n",
    "            let mut inner = inner.lock().unwrap();\n",
    "            inner.last_activity = Instant::now();\n",
    "            if sending {\n",
    "                inner.bytes_sent += bytes.len() as u32;\n",
    "            } else {\n",
    "                inner.bytes_received += bytes.len() as u32;\n",
    "            }\n",
    "            Ok(bytes)\n",
    "        }\n",
    "        err => err.or_http_err(StatusCode::SEE_OTHER, \"stream failure\"),\n",
    "    })\n",
    "    \n",
    "}\n",
    "```\n",
    "- and_then map match\n",
    "- |v| ?\n",
    "- where impl trait"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e63eb4d",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}