deroise2306

My change

May 26th, 2026 (edited)
12
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.88 KB | Cybersecurity | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. l2cap2wav.py — Reconstruct audio from Bluetooth L2CAP/A2DP captures
  4.  
  5. Accepts either:
  6. - A raw pcap/pcapng file (tshark is run automatically)
  7. - A pre-exported tshark text file (tab-separated fields)
  8.  
  9. Assumes:
  10. - The audio CID carries RTP-encapsulated SBC frames (A2DP media)
  11. - RTP header = 12 bytes, SBC frame count = 1 byte, then raw SBC frames
  12. - Codec: SBC (decoded via ffmpeg)
  13.  
  14. Usage:
  15. python3 l2cap2wav.py <capture.pcapng|profiles.txt> [audio_cid] [output.wav]
  16.  
  17. audio_cid defaults to 0x0052 (typical A2DP dynamic CID)
  18. Pass 'auto' as audio_cid to scan all CIDs and pick the best candidate
  19. """
  20.  
  21. import sys
  22. import subprocess
  23. import shutil
  24. import tempfile
  25. from pathlib import Path
  26. from collections import defaultdict
  27.  
  28.  
  29. if hasattr(sys.stdout, "reconfigure"):
  30. sys.stdout.reconfigure(encoding="utf-8", errors="replace")
  31.  
  32.  
  33. RTP_HEADER_SIZE = 12 # bytes
  34. SBC_COUNT_SIZE = 1 # byte (number of SBC frames in RTP payload)
  35. AAC_LATM_SYNC = bytes.fromhex("47 fc 00 00 b0 8c 80 03")
  36.  
  37. PCAP_MAGIC_BYTES = {
  38. b'\xd4\xc3\xb2\xa1', # pcap LE
  39. b'\xa1\xb2\xc3\xd4', # pcap BE
  40. b'\x0a\x0d\x0d\x0a', # pcapng
  41. }
  42.  
  43.  
  44. def is_pcap(path: Path) -> bool:
  45. """Detect pcap/pcapng by magic bytes."""
  46. try:
  47. magic = path.read_bytes()[:4]
  48. return magic in PCAP_MAGIC_BYTES
  49. except OSError:
  50. return False
  51.  
  52.  
  53. def find_tshark() -> Path | None:
  54. """Locate tshark via PATH or common install locations."""
  55. via_path = shutil.which('tshark')
  56. if via_path:
  57. return Path(via_path)
  58. candidates = [
  59. Path(r'C:\Program Files\Wireshark\tshark.exe'),
  60. Path(r'C:\Program Files (x86)\Wireshark\tshark.exe'),
  61. Path('/usr/bin/tshark'),
  62. Path('/usr/local/bin/tshark'),
  63. Path('/Applications/Wireshark.app/Contents/MacOS/tshark'),
  64. Path('/bin/tshark'),
  65. Path('/opt/local/bin/tshark'),
  66. ]
  67. for p in candidates:
  68. if p.exists():
  69. return p
  70. return None
  71.  
  72.  
  73. def tshark_export(pcap: Path, out_txt: Path) -> None:
  74. """Run tshark to export L2CAP fields from a pcap to a text file."""
  75. tshark = find_tshark()
  76. if not tshark:
  77. print("[ERROR] tshark not found. Install Wireshark/tshark or export manually:")
  78. print(" tshark -r capture.pcapng -T fields \\")
  79. print(" -e frame.number -e btl2cap.cid \\")
  80. print(" -e btl2cap.length -e btl2cap.payload \\")
  81. print(" -Y btl2cap > profiles.txt")
  82. print(" If -r <file> fails on permissions: cat capture.pcapng | tshark -r - ...")
  83. sys.exit(1)
  84.  
  85. print(f" tshark found at: {tshark}")
  86. print(f" Exporting L2CAP fields from {pcap.name}...")
  87. fields_args = [
  88. '-T', 'fields',
  89. '-e', 'frame.number',
  90. '-e', 'btl2cap.cid',
  91. '-e', 'btl2cap.length',
  92. '-e', 'btl2cap.payload',
  93. '-Y', 'btl2cap',
  94. ]
  95. result = subprocess.run(
  96. [str(tshark), '-r', str(pcap), *fields_args],
  97. capture_output=True,
  98. text=True,
  99. )
  100. if result.returncode != 0:
  101. print(" Direct -r <file> failed; retrying via stdin (cat | tshark -r -)...")
  102. cat = subprocess.Popen(['cat', str(pcap)], stdout=subprocess.PIPE)
  103. try:
  104. result = subprocess.run(
  105. [str(tshark), '-r', '-', *fields_args],
  106. stdin=cat.stdout,
  107. capture_output=True,
  108. text=True,
  109. )
  110. finally:
  111. if cat.stdout:
  112. cat.stdout.close()
  113. cat.wait()
  114. if result.returncode != 0:
  115. print(f"[ERROR] tshark failed:\n{result.stderr[-500:]}")
  116. sys.exit(1)
  117.  
  118. out_txt.write_text(result.stdout)
  119. n_lines = result.stdout.count('\n')
  120. print(f" Exported {n_lines} L2CAP packets → {out_txt.name}")
  121.  
  122.  
  123. def sniff_best_cid(profiles_path: Path) -> str:
  124. """Scan all CIDs and return the one most likely to carry A2DP audio."""
  125. from collections import defaultdict
  126. cid_stats: dict[str, dict] = defaultdict(lambda: {'count': 0, 'total_len': 0})
  127.  
  128. with open(profiles_path) as f:
  129. for line in f:
  130. parts = line.split()
  131. if len(parts) < 4:
  132. continue
  133. cid = parts[1].lower()
  134. try:
  135. length = int(parts[2])
  136. except ValueError:
  137. continue
  138. cid_stats[cid]['count'] += 1
  139. cid_stats[cid]['total_len'] += length
  140.  
  141. print(" CID scan results:")
  142. best_cid, best_score = '0x0052', 0
  143. for cid, s in sorted(cid_stats.items(), key=lambda x: -x[1]['count']):
  144. avg_len = s['total_len'] / s['count'] if s['count'] else 0
  145. # High packet count + large avg payload = likely media stream
  146. score = s['count'] * avg_len
  147. print(f" {cid:>8} packets={s['count']:>5} avg_len={avg_len:>7.1f} score={score:,.0f}")
  148. if score > best_score:
  149. best_score = score
  150. best_cid = cid
  151.  
  152. print(f" → Best candidate: {best_cid}")
  153. return best_cid
  154.  
  155.  
  156. def parse_profiles(path: Path, audio_cid: str) -> tuple[bytearray, list[int]]:
  157. """Extract raw SBC bytestream from L2CAP profile export."""
  158. sbc_stream = bytearray()
  159. seq_numbers = []
  160. missing_seqs = []
  161.  
  162. with open(path) as f:
  163. for line in f:
  164. parts = line.split()
  165. if len(parts) < 4 or parts[1].lower() != audio_cid.lower():
  166. continue
  167.  
  168. try:
  169. raw = bytes.fromhex(parts[3])
  170. except ValueError:
  171. print(f" [WARN] Frame {parts[0]}: invalid hex payload, skipping")
  172. continue
  173.  
  174. # Validate RTP version
  175. if (raw[0] >> 6) != 2:
  176. print(f" [WARN] Frame {parts[0]}: not RTP v2 (byte={raw[0]:#04x}), skipping")
  177. continue
  178.  
  179. rtp_seq = int.from_bytes(raw[2:4], 'big')
  180. rtp_ts = int.from_bytes(raw[4:8], 'big')
  181. n_frames = raw[RTP_HEADER_SIZE]
  182.  
  183. # Check for out-of-order or missing packets
  184. if seq_numbers:
  185. expected = seq_numbers[-1] + 1
  186. if rtp_seq != expected:
  187. missing = rtp_seq - expected
  188. missing_seqs.append((rtp_seq, missing))
  189. print(f" [WARN] Gap before RTP seq {rtp_seq}: {missing} packet(s) missing")
  190.  
  191. seq_numbers.append(rtp_seq)
  192. sbc_payload = raw[RTP_HEADER_SIZE + SBC_COUNT_SIZE:]
  193. sbc_stream.extend(sbc_payload)
  194.  
  195. return sbc_stream, seq_numbers, missing_seqs
  196.  
  197.  
  198. def detect_sbc_params(sbc_stream: bytes) -> dict:
  199. """Parse SBC frame header to extract codec parameters."""
  200. if len(sbc_stream) < 4 or sbc_stream[0] != 0x9c:
  201. return {}
  202.  
  203. hdr = sbc_stream[1]
  204. sf_map = {0: '16000', 1: '32000', 2: '44100', 3: '48000'}
  205. blk_map = {0: 4, 1: 8, 2: 12, 3: 16}
  206. cm_map = {0: 'mono', 1: 'dual', 2: 'stereo', 3: 'joint_stereo'}
  207. sbn_map = {0: 4, 1: 8}
  208. am_map = {0: 'loudness', 1: 'SNR'}
  209.  
  210. return {
  211. 'sample_rate': sf_map[(hdr >> 6) & 3],
  212. 'blocks': blk_map[(hdr >> 4) & 3],
  213. 'channel_mode': cm_map[(hdr >> 2) & 3],
  214. 'alloc_method': am_map[(hdr >> 1) & 1],
  215. 'subbands': sbn_map[hdr & 1],
  216. 'bitpool': sbc_stream[2],
  217. }
  218.  
  219.  
  220. def decode_sbc_to_wav(sbc_path: Path, wav_path: Path) -> bool:
  221. """Use ffmpeg to decode raw SBC stream to WAV."""
  222. ffmpeg = shutil.which('ffmpeg')
  223. if not ffmpeg:
  224. print("[ERROR] ffmpeg not found — install it to decode SBC to WAV")
  225. print(f" Raw SBC saved to: {sbc_path}")
  226. print(" You can also use: sbcdec (from bluez-tools) or VLC")
  227. return False
  228.  
  229. result = subprocess.run(
  230. [ffmpeg, '-hide_banner', '-y', '-f', 'sbc', '-i', str(sbc_path), str(wav_path)],
  231. capture_output=True, text=True
  232. )
  233.  
  234. if wav_path.exists() and wav_path.stat().st_size > 0:
  235. return True
  236. else:
  237. print(f"[ERROR] ffmpeg failed:\n{result.stderr[-500:]}")
  238. return False
  239.  
  240.  
  241. def loas_header(n: int) -> bytes:
  242. if not 0 <= n <= 0x1FFF:
  243. raise ValueError(f"LOAS payload too long: {n}")
  244. return ((0x2B7 << 13) | n).to_bytes(3, "big")
  245.  
  246.  
  247. def parse_int_auto(value: str) -> int:
  248. return int(value, 0)
  249.  
  250.  
  251. def parse_extra_args(args: list[str]) -> dict:
  252. opts = {
  253. "codec": "sbc",
  254. "direction": None,
  255. "handle": None,
  256. "all": False,
  257. "out_dir": None,
  258. }
  259. i = 0
  260. while i < len(args):
  261. arg = args[i]
  262. if arg == "--codec" and i + 1 < len(args):
  263. opts["codec"] = args[i + 1].lower()
  264. i += 2
  265. elif arg == "--direction" and i + 1 < len(args):
  266. opts["direction"] = parse_int_auto(args[i + 1])
  267. i += 2
  268. elif arg == "--handle" and i + 1 < len(args):
  269. opts["handle"] = parse_int_auto(args[i + 1])
  270. i += 2
  271. elif arg == "--aac-latm":
  272. opts["codec"] = "aac-latm"
  273. i += 1
  274. elif arg == "--all":
  275. opts["all"] = True
  276. i += 1
  277. elif arg == "--out-dir" and i + 1 < len(args):
  278. opts["out_dir"] = Path(args[i + 1])
  279. i += 2
  280. else:
  281. print(f"[ERROR] Unknown option: {arg}")
  282. sys.exit(1)
  283. return opts
  284.  
  285.  
  286. def decode_loas_to_wav(loas_path: Path, wav_path: Path) -> bool:
  287. ffmpeg = shutil.which("ffmpeg")
  288. if not ffmpeg:
  289. print("[ERROR] ffmpeg not found")
  290. return False
  291.  
  292. result = subprocess.run(
  293. [
  294. ffmpeg,
  295. "-hide_banner",
  296. "-y",
  297. "-f",
  298. "loas",
  299. "-i",
  300. str(loas_path),
  301. "-ac",
  302. "1",
  303. "-ar",
  304. "8000",
  305. str(wav_path),
  306. ],
  307. capture_output=True,
  308. text=True,
  309. )
  310. if wav_path.exists() and wav_path.stat().st_size > 0:
  311. return True
  312. print(f"[ERROR] ffmpeg failed:\n{result.stderr[-500:]}")
  313. return False
  314.  
  315.  
  316. def extract_aac_latm_to_wav(
  317. pcap_path: Path,
  318. audio_cid: str,
  319. wav_path: Path,
  320. direction: int | None = None,
  321. handle: int | None = None,
  322. ) -> bool:
  323. from extract_audio_from_pcap_only import iter_l2cap_sdus, parse_pcapng
  324.  
  325. cid_value = parse_int_auto(audio_cid)
  326. _, _, packets, _ = parse_pcapng(pcap_path)
  327.  
  328. payloads = []
  329. seqs = []
  330. for sdu in iter_l2cap_sdus(packets):
  331. if sdu["cid"] != cid_value:
  332. continue
  333. if direction is not None and sdu["direction"] != direction:
  334. continue
  335. if handle is not None and sdu["handle"] != handle:
  336. continue
  337.  
  338. raw = sdu["payload"]
  339. if len(raw) <= RTP_HEADER_SIZE or (raw[0] >> 6) != 2:
  340. continue
  341. media = raw[RTP_HEADER_SIZE:]
  342. if not media.startswith(AAC_LATM_SYNC):
  343. continue
  344. payloads.append(media)
  345. seqs.append(int.from_bytes(raw[2:4], "big"))
  346.  
  347. if not payloads:
  348. print("[ERROR] No AAC/LATM RTP payloads found. Check CID/handle/direction.")
  349. return False
  350.  
  351. loas_path = wav_path.with_suffix(".loas")
  352. with loas_path.open("wb") as f:
  353. for payload in payloads:
  354. f.write(loas_header(len(payload)))
  355. f.write(payload)
  356.  
  357. gaps = 0
  358. for prev, cur in zip(seqs, seqs[1:]):
  359. if ((prev + 1) & 0xFFFF) != cur:
  360. gaps += 1
  361.  
  362. print(f" RTP packets : {len(payloads)}")
  363. print(f" RTP gaps : {gaps}")
  364. print(f" LOAS saved : {loas_path}")
  365. return decode_loas_to_wav(loas_path, wav_path)
  366.  
  367.  
  368. def is_rtp_payload(raw: bytes) -> bool:
  369. return len(raw) > RTP_HEADER_SIZE and (raw[0] >> 6) == 2
  370.  
  371.  
  372. def rtp_seq(raw: bytes) -> int:
  373. return int.from_bytes(raw[2:4], "big")
  374.  
  375.  
  376. def count_rtp_gaps(payloads: list[bytes]) -> int:
  377. seqs = [rtp_seq(p) for p in payloads]
  378. gaps = 0
  379. for prev, cur in zip(seqs, seqs[1:]):
  380. if ((prev + 1) & 0xFFFF) != cur:
  381. gaps += 1
  382. return gaps
  383.  
  384.  
  385. def detect_a2dp_codec(payloads: list[bytes]) -> str | None:
  386. if not payloads:
  387. return None
  388. media = [p[RTP_HEADER_SIZE:] for p in payloads if len(p) > RTP_HEADER_SIZE]
  389. if not media:
  390. return None
  391.  
  392. aac = sum(m.startswith(AAC_LATM_SYNC) for m in media)
  393. if aac >= max(5, int(len(media) * 0.80)):
  394. return "aac-latm"
  395.  
  396. # A2DP/SBC RTP media payload normally has one byte frame count, then SBC
  397. # frames beginning with syncword 0x9c.
  398. sbc = sum(len(m) > 1 and 1 <= m[0] <= 15 and m[1] == 0x9C for m in media)
  399. if sbc >= max(5, int(len(media) * 0.80)):
  400. return "sbc"
  401.  
  402. return None
  403.  
  404.  
  405. def export_aac_group(payloads: list[bytes], base: Path) -> bool:
  406. loas_path = base.with_suffix(".loas")
  407. wav_path = base.with_suffix(".wav")
  408. with loas_path.open("wb") as f:
  409. for raw in payloads:
  410. media = raw[RTP_HEADER_SIZE:]
  411. if media.startswith(AAC_LATM_SYNC):
  412. f.write(loas_header(len(media)))
  413. f.write(media)
  414. return decode_loas_to_wav(loas_path, wav_path)
  415.  
  416.  
  417. def export_sbc_group(payloads: list[bytes], base: Path) -> bool:
  418. sbc_path = base.with_suffix(".sbc")
  419. wav_path = base.with_suffix(".wav")
  420. sbc_stream = bytearray()
  421. for raw in payloads:
  422. media = raw[RTP_HEADER_SIZE:]
  423. if len(media) > SBC_COUNT_SIZE:
  424. sbc_stream.extend(media[SBC_COUNT_SIZE:])
  425. sbc_path.write_bytes(bytes(sbc_stream))
  426. return decode_sbc_to_wav(sbc_path, wav_path)
  427.  
  428.  
  429. def extract_all_audio(pcap_path: Path, out_dir: Path) -> list[dict]:
  430. from extract_audio_from_pcap_only import iter_l2cap_sdus, parse_pcapng
  431.  
  432. _, _, packets, _ = parse_pcapng(pcap_path)
  433. groups: dict[tuple[int, int, int], list[bytes]] = defaultdict(list)
  434. for sdu in iter_l2cap_sdus(packets):
  435. raw = sdu["payload"]
  436. if is_rtp_payload(raw):
  437. groups[(sdu["direction"], sdu["handle"], sdu["cid"])].append(raw)
  438.  
  439. out_dir.mkdir(parents=True, exist_ok=True)
  440. results = []
  441. for gi, (key, payloads) in enumerate(sorted(groups.items()), 1):
  442. if len(payloads) < 10:
  443. continue
  444. direction, handle, cid = key
  445. codec = detect_a2dp_codec(payloads)
  446. if not codec:
  447. continue
  448.  
  449. stem = f"grp_{gi:03d}_dir{direction}_h{handle:04x}_cid{cid:04x}_{codec}"
  450. base = out_dir / stem
  451. print(f"[{stem}] packets={len(payloads)} gaps={count_rtp_gaps(payloads)}")
  452. if codec == "aac-latm":
  453. ok = export_aac_group(payloads, base)
  454. elif codec == "sbc":
  455. ok = export_sbc_group(payloads, base)
  456. else:
  457. ok = False
  458.  
  459. wav_path = base.with_suffix(".wav")
  460. results.append(
  461. {
  462. "name": stem,
  463. "codec": codec,
  464. "direction": direction,
  465. "handle": f"0x{handle:04x}",
  466. "cid": f"0x{cid:04x}",
  467. "packets": len(payloads),
  468. "gaps": count_rtp_gaps(payloads),
  469. "ok": ok,
  470. "wav": str(wav_path) if wav_path.exists() else "",
  471. "wav_bytes": wav_path.stat().st_size if wav_path.exists() else 0,
  472. }
  473. )
  474. return results
  475.  
  476.  
  477. def main():
  478. if len(sys.argv) < 2:
  479. print(__doc__)
  480. sys.exit(1)
  481.  
  482. argv = sys.argv[1:]
  483. input_path = Path(argv[0])
  484. pos_args = []
  485. extra_args = []
  486. for idx, arg in enumerate(argv[1:], 1):
  487. if arg.startswith("--"):
  488. extra_args = argv[idx:]
  489. break
  490. pos_args.append(arg)
  491. audio_cid = pos_args[0] if len(pos_args) > 0 else "0x0052"
  492. output_wav = Path(pos_args[1]) if len(pos_args) > 1 else input_path.with_suffix(".wav")
  493. output_sbc = output_wav.with_suffix('.sbc')
  494. opts = parse_extra_args(extra_args)
  495.  
  496. print(f"╔══════════════════════════════════════╗")
  497. print(f"║ L2CAP A2DP SBC → WAV Extractor ║")
  498. print(f"╚══════════════════════════════════════╝")
  499. print(f"Input : {input_path}")
  500. print(f"Output: {output_wav}\n")
  501.  
  502. if opts["all"]:
  503. if not is_pcap(input_path):
  504. print("[ERROR] --all expects the original pcap/pcapng input")
  505. sys.exit(1)
  506. out_dir = opts["out_dir"] or output_wav
  507. if out_dir.suffix:
  508. out_dir = out_dir.with_suffix("")
  509. print(f"[ALL] Scanning all RTP-looking L2CAP streams -> {out_dir}")
  510. results = extract_all_audio(input_path, out_dir)
  511. ok_count = sum(1 for r in results if r["ok"])
  512. print(f"\nDone. decoded={ok_count}/{len(results)} candidate audio streams")
  513. for r in results:
  514. status = "OK" if r["ok"] else "FAIL"
  515. print(
  516. f" {status} {r['name']} codec={r['codec']} packets={r['packets']} "
  517. f"gaps={r['gaps']} wav={r['wav']}"
  518. )
  519. sys.exit(0 if ok_count else 1)
  520.  
  521. if opts["codec"] in {"aac", "aac-latm", "latm", "loas"}:
  522. if not is_pcap(input_path):
  523. print("[ERROR] AAC/LATM mode currently expects the original pcap/pcapng input")
  524. sys.exit(1)
  525. print("[AAC/LATM] Reconstructing RTP media payloads and wrapping as LOAS...")
  526. ok = extract_aac_latm_to_wav(
  527. input_path,
  528. audio_cid,
  529. output_wav,
  530. direction=opts["direction"],
  531. handle=opts["handle"],
  532. )
  533. if ok:
  534. size_kb = output_wav.stat().st_size // 1024
  535. print(f" Done! WAV saved: {output_wav} ({size_kb} KB)")
  536. sys.exit(0)
  537. sys.exit(1)
  538.  
  539. # --- Step 0: If input is a pcap, run tshark first ---
  540. profiles_path = input_path
  541. _tmpdir = None
  542. if is_pcap(input_path):
  543. print("[0/3] pcap/pcapng detected — running tshark export...")
  544. _tmpdir = tempfile.TemporaryDirectory()
  545. profiles_path = Path(_tmpdir.name) / 'profiles.txt'
  546. tshark_export(input_path, profiles_path)
  547. print()
  548.  
  549. # --- Auto CID detection ---
  550. if audio_cid.lower() == 'auto':
  551. print("[CID] Scanning for best audio channel...")
  552. audio_cid = sniff_best_cid(profiles_path)
  553. print()
  554.  
  555. print(f"CID : {audio_cid}\n")
  556.  
  557. # --- Step 1: Extract SBC stream ---
  558. print("[1/3] Parsing L2CAP packets...")
  559. sbc_stream, seqs, gaps = parse_profiles(profiles_path, audio_cid)
  560.  
  561. if not sbc_stream:
  562. print(f"[ERROR] No packets found for CID {audio_cid}")
  563. sys.exit(1)
  564.  
  565. print(f" RTP packets : {len(seqs)}")
  566. print(f" Missing gaps : {len(gaps)}")
  567. print(f" SBC bytes : {len(sbc_stream):,}")
  568.  
  569. # --- Step 2: Show SBC params ---
  570. print("\n[2/3] Detecting SBC parameters...")
  571. params = detect_sbc_params(bytes(sbc_stream))
  572. if params:
  573. duration_est = len(seqs) * 7 * params['blocks'] * params['subbands'] / int(params['sample_rate'])
  574. print(f" Sample rate : {params['sample_rate']} Hz")
  575. print(f" Channel mode : {params['channel_mode']}")
  576. print(f" Blocks : {params['blocks']}")
  577. print(f" Subbands : {params['subbands']}")
  578. print(f" Bitpool : {params['bitpool']}")
  579. print(f" Est. duration: {duration_est:.2f}s")
  580.  
  581. # Write raw SBC (always useful for manual inspection)
  582. output_sbc.write_bytes(bytes(sbc_stream))
  583. print(f" Raw SBC saved: {output_sbc}")
  584.  
  585. # --- Step 3: Decode to WAV ---
  586. print("\n[3/3] Decoding SBC → WAV...")
  587. if decode_sbc_to_wav(output_sbc, output_wav):
  588. size_kb = output_wav.stat().st_size // 1024
  589. print(f" Done! WAV saved: {output_wav} ({size_kb} KB)")
  590. else:
  591. sys.exit(1)
  592.  
  593. if _tmpdir:
  594. _tmpdir.cleanup()
  595.  
  596.  
  597. if __name__ == '__main__':
  598. main()
  599.  
Advertisement
Add Comment
Please, Sign In to add comment