mini2fa 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mini2fa-0.2.1/src/mini2fa.egg-info → mini2fa-0.2.3}/PKG-INFO +1 -1
- {mini2fa-0.2.1 → mini2fa-0.2.3}/pyproject.toml +1 -1
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/__init__.py +1 -1
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/_cli.py +145 -2
- {mini2fa-0.2.1 → mini2fa-0.2.3/src/mini2fa.egg-info}/PKG-INFO +1 -1
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_cli.py +4 -2
- {mini2fa-0.2.1 → mini2fa-0.2.3}/LICENSE +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/README.md +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/setup.cfg +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/__main__.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/config.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/crypto.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/models.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/scanner.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/storage.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/totp.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa/ui.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa.egg-info/SOURCES.txt +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa.egg-info/dependency_links.txt +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa.egg-info/entry_points.txt +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa.egg-info/requires.txt +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/src/mini2fa.egg-info/top_level.txt +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_config.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_crypto.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_models.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_scanner.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_storage.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_totp.py +0 -0
- {mini2fa-0.2.1 → mini2fa-0.2.3}/tests/test_ui.py +0 -0
|
@@ -156,6 +156,7 @@ def handle_add_account(storage: StorageManager):
|
|
|
156
156
|
|
|
157
157
|
if not os.path.exists(image_path):
|
|
158
158
|
ui.print_line("✗ 文件不存在!请检查路径。")
|
|
159
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
159
160
|
return
|
|
160
161
|
|
|
161
162
|
ui.print_line("正在扫描二维码...")
|
|
@@ -164,14 +165,17 @@ def handle_add_account(storage: StorageManager):
|
|
|
164
165
|
result = scan_qrcode(image_path)
|
|
165
166
|
except UnsupportedOTPTypeError as e:
|
|
166
167
|
ui.print_line(f"✗ 该二维码是 {e.otp_type.upper()} 协议,本工具仅支持 TOTP。")
|
|
168
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
167
169
|
return
|
|
168
170
|
except Exception as e:
|
|
169
171
|
ui.print_line(f"✗ 扫描失败: {e}")
|
|
172
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
170
173
|
return
|
|
171
174
|
|
|
172
175
|
if not result:
|
|
173
176
|
ui.print_line("✗ 未识别到有效的二维码!")
|
|
174
177
|
ui.print_line(" 提示:请确保图片包含清晰的 OTP 二维码。")
|
|
178
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
175
179
|
return
|
|
176
180
|
|
|
177
181
|
# 查重:已存在则直接提示,不进入填字段流程
|
|
@@ -180,6 +184,7 @@ def handle_add_account(storage: StorageManager):
|
|
|
180
184
|
ui.print_line(f"✗ 账号已存在: {result.issuer} - {result.account}")
|
|
181
185
|
ui.print_line(" 该账号已在库中(分类: {0}, ID: {1})。".format(
|
|
182
186
|
existing.category, existing.id))
|
|
187
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
183
188
|
return
|
|
184
189
|
|
|
185
190
|
# 显示识别结果
|
|
@@ -296,6 +301,7 @@ def handle_view_code(storage: StorageManager):
|
|
|
296
301
|
idx = int(raw)
|
|
297
302
|
if idx < 1 or idx > len(accounts):
|
|
298
303
|
ui.print_line("✗ 无效的选择!")
|
|
304
|
+
ui.prompt("\n按 Enter 继续 ")
|
|
299
305
|
continue
|
|
300
306
|
account = accounts[idx - 1]
|
|
301
307
|
else:
|
|
@@ -310,6 +316,7 @@ def handle_view_code(storage: StorageManager):
|
|
|
310
316
|
searching = True
|
|
311
317
|
if not accounts:
|
|
312
318
|
ui.print_line("✗ 未找到匹配账号,已返回全量列表。")
|
|
319
|
+
ui.prompt("\n按 Enter 继续 ")
|
|
313
320
|
accounts = all_accounts
|
|
314
321
|
searching = False
|
|
315
322
|
continue
|
|
@@ -317,6 +324,7 @@ def handle_view_code(storage: StorageManager):
|
|
|
317
324
|
secret = storage.get_secret(account.id)
|
|
318
325
|
except Exception as e:
|
|
319
326
|
ui.print_line(f"✗ 获取密钥失败: {e}")
|
|
327
|
+
ui.prompt("\n按 Enter 继续 ")
|
|
320
328
|
continue
|
|
321
329
|
|
|
322
330
|
# 进入详情页
|
|
@@ -353,9 +361,11 @@ def handle_edit_account(storage: StorageManager):
|
|
|
353
361
|
idx = int(idx)
|
|
354
362
|
if idx < 1 or idx > len(accounts):
|
|
355
363
|
ui.print_line("✗ 无效的选择!")
|
|
364
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
356
365
|
return
|
|
357
366
|
except ValueError:
|
|
358
367
|
ui.print_line("✗ 请输入数字!")
|
|
368
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
359
369
|
return
|
|
360
370
|
|
|
361
371
|
account = accounts[idx - 1]
|
|
@@ -421,9 +431,11 @@ def handle_delete_account(storage: StorageManager):
|
|
|
421
431
|
idx = int(idx)
|
|
422
432
|
if idx < 1 or idx > len(accounts):
|
|
423
433
|
ui.print_line("✗ 无效的选择!")
|
|
434
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
424
435
|
return
|
|
425
436
|
except ValueError:
|
|
426
437
|
ui.print_line("✗ 请输入数字!")
|
|
438
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
427
439
|
return
|
|
428
440
|
|
|
429
441
|
account = accounts[idx - 1]
|
|
@@ -474,6 +486,7 @@ def handle_export(storage: StorageManager):
|
|
|
474
486
|
invalid_chars = set('*?<>|"')
|
|
475
487
|
if user_path and any(c in os.path.basename(backup_path) for c in invalid_chars):
|
|
476
488
|
ui.print_line("✗ 文件名包含非法字符(* ? < > | \"),请重新输入。")
|
|
489
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
477
490
|
return
|
|
478
491
|
|
|
479
492
|
# 目标文件已存在时确认覆盖
|
|
@@ -513,6 +526,7 @@ def handle_change_password(crypto: CryptoManager):
|
|
|
513
526
|
ui.print_line(f" 剩余 {2 - attempt} 次尝试。")
|
|
514
527
|
else:
|
|
515
528
|
ui.print_line("✗ 验证失败次数过多,已返回主菜单。")
|
|
529
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
516
530
|
return
|
|
517
531
|
|
|
518
532
|
ui.print_line("✓ 旧密码验证通过!")
|
|
@@ -538,6 +552,7 @@ def handle_change_password(crypto: CryptoManager):
|
|
|
538
552
|
else:
|
|
539
553
|
ui.print_line(f"\n✗ 新密码设置失败:已尝试 {MAX_PASSWORD_ATTEMPTS} 次。")
|
|
540
554
|
ui.print_line(" 请重新运行 mini2fa 重试。")
|
|
555
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
541
556
|
return
|
|
542
557
|
|
|
543
558
|
# 3. 重设密保提示(回车保留原提示)
|
|
@@ -571,6 +586,7 @@ def handle_import(storage: StorageManager):
|
|
|
571
586
|
|
|
572
587
|
if not input_path:
|
|
573
588
|
ui.print_line("✗ 路径不能为空!")
|
|
589
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
574
590
|
return
|
|
575
591
|
|
|
576
592
|
# 移除路径两端的引号
|
|
@@ -583,10 +599,12 @@ def handle_import(storage: StorageManager):
|
|
|
583
599
|
invalid_chars = set('*?<>|"')
|
|
584
600
|
if any(c in os.path.basename(input_path) for c in invalid_chars):
|
|
585
601
|
ui.print_line("✗ 文件名包含非法字符(* ? < > | \"),请重新输入。")
|
|
602
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
586
603
|
return
|
|
587
604
|
|
|
588
605
|
if not os.path.exists(input_path):
|
|
589
606
|
ui.print_line("✗ 文件不存在!")
|
|
607
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
590
608
|
return
|
|
591
609
|
|
|
592
610
|
try:
|
|
@@ -604,9 +622,11 @@ def handle_import(storage: StorageManager):
|
|
|
604
622
|
external_key = storage.crypto.load_external_key(embedded_key, bk_pwd)
|
|
605
623
|
except ValueError as e:
|
|
606
624
|
ui.print_line(f"✗ {e}")
|
|
625
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
607
626
|
return
|
|
608
627
|
if external_key is None:
|
|
609
628
|
ui.print_line("✗ 备份主密码错误!")
|
|
629
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
610
630
|
return
|
|
611
631
|
ui.print_line("✓ 备份主密码验证通过!")
|
|
612
632
|
|
|
@@ -615,6 +635,7 @@ def handle_import(storage: StorageManager):
|
|
|
615
635
|
|
|
616
636
|
if preview['total'] == 0:
|
|
617
637
|
ui.print_line("✗ 备份文件中没有账号数据。")
|
|
638
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
618
639
|
return
|
|
619
640
|
|
|
620
641
|
ui.print_line("\n📋 备份文件预览:")
|
|
@@ -678,12 +699,14 @@ def handle_import(storage: StorageManager):
|
|
|
678
699
|
_print_import_result(result)
|
|
679
700
|
except json.JSONDecodeError:
|
|
680
701
|
ui.print_line("✗ 导入失败:备份文件格式错误,不是有效的 JSON 文件。")
|
|
702
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
681
703
|
except Exception as e:
|
|
682
704
|
err_msg = str(e)
|
|
683
705
|
if 'InvalidTag' in err_msg or 'decrypt' in err_msg.lower():
|
|
684
706
|
ui.print_line("✗ 导入失败:无法解密备份文件,可能是主密码不匹配或文件已损坏。")
|
|
685
707
|
else:
|
|
686
708
|
ui.print_line(f"✗ 导入失败: {e}")
|
|
709
|
+
ui.prompt("\n按 Enter 返回主菜单 ")
|
|
687
710
|
|
|
688
711
|
|
|
689
712
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -701,6 +724,25 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
701
724
|
'--version', action='version', version=f'mini2fa {__version__}',
|
|
702
725
|
help='显示版本号并退出',
|
|
703
726
|
)
|
|
727
|
+
|
|
728
|
+
# 子命令
|
|
729
|
+
sub = parser.add_subparsers(dest='command', help='子命令')
|
|
730
|
+
|
|
731
|
+
# list 子命令(-p 在子命令后面)
|
|
732
|
+
list_cmd = sub.add_parser('list', help='列出所有账号(ID + issuer + account)')
|
|
733
|
+
list_cmd.add_argument(
|
|
734
|
+
'-p', '--password', metavar='PASSWORD', required=True,
|
|
735
|
+
help='主密码',
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# get 子命令(-p 在子命令后面)
|
|
739
|
+
get_cmd = sub.add_parser('get', help='获取验证码')
|
|
740
|
+
get_cmd.add_argument(
|
|
741
|
+
'-p', '--password', metavar='PASSWORD', required=True,
|
|
742
|
+
help='主密码',
|
|
743
|
+
)
|
|
744
|
+
get_cmd.add_argument('id', type=int, help='账号 ID(从 list 获取)')
|
|
745
|
+
|
|
704
746
|
return parser
|
|
705
747
|
|
|
706
748
|
|
|
@@ -709,11 +751,22 @@ def main(argv=None):
|
|
|
709
751
|
|
|
710
752
|
Args:
|
|
711
753
|
argv: 命令行参数列表(None 时用 sys.argv[1:])。
|
|
712
|
-
由 argparse 解析,支持 --no-tui
|
|
754
|
+
由 argparse 解析,支持 --no-tui、-p、子命令 list。
|
|
713
755
|
|
|
714
756
|
Ctrl-C 在任意阶段(含主密码输入)都被捕获,优雅退出。
|
|
715
757
|
"""
|
|
716
758
|
args = _build_parser().parse_args(argv)
|
|
759
|
+
|
|
760
|
+
# 子命令模式:直接执行,不进 TUI
|
|
761
|
+
if args.command == 'list':
|
|
762
|
+
_handle_list(args.password)
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
if args.command == 'get':
|
|
766
|
+
_handle_get(args.password, args.id)
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
# 交互模式(TUI)
|
|
717
770
|
ui.configure(no_tui=args.no_tui)
|
|
718
771
|
ui.enter()
|
|
719
772
|
try:
|
|
@@ -726,6 +779,88 @@ def main(argv=None):
|
|
|
726
779
|
ui.leave()
|
|
727
780
|
|
|
728
781
|
|
|
782
|
+
def _handle_list(password: str):
|
|
783
|
+
"""子命令 list:列出所有账号(ID + issuer + account)
|
|
784
|
+
|
|
785
|
+
零交互:密码通过参数传入,输出纯文本。
|
|
786
|
+
"""
|
|
787
|
+
# 初始化数据目录(确保存在)
|
|
788
|
+
init_data_dir()
|
|
789
|
+
|
|
790
|
+
db_path = str(get_db_path())
|
|
791
|
+
key_path = str(get_key_path())
|
|
792
|
+
|
|
793
|
+
# 检查主密钥文件是否存在
|
|
794
|
+
if not os.path.exists(key_path):
|
|
795
|
+
print("错误: 首次使用请先运行 mini2fa 设置主密码", file=sys.stderr)
|
|
796
|
+
sys.exit(1)
|
|
797
|
+
|
|
798
|
+
crypto = CryptoManager(key_path)
|
|
799
|
+
|
|
800
|
+
# 验证密码(不污染会话)
|
|
801
|
+
if not crypto.initialize(password):
|
|
802
|
+
print("密码错误", file=sys.stderr)
|
|
803
|
+
sys.exit(1)
|
|
804
|
+
|
|
805
|
+
storage = StorageManager(db_path, crypto)
|
|
806
|
+
accounts = storage.get_all_accounts()
|
|
807
|
+
|
|
808
|
+
if not accounts:
|
|
809
|
+
print("暂无账号", file=sys.stderr)
|
|
810
|
+
sys.exit(0)
|
|
811
|
+
|
|
812
|
+
# 按分类分组,输出 ID + issuer + account
|
|
813
|
+
groups = {}
|
|
814
|
+
for acc in accounts:
|
|
815
|
+
groups.setdefault(acc.category, []).append(acc)
|
|
816
|
+
|
|
817
|
+
categories = sorted(groups.keys(), key=lambda c: (c != 'default', c))
|
|
818
|
+
|
|
819
|
+
for category in categories:
|
|
820
|
+
print(f"\n [{category}]")
|
|
821
|
+
for acc in groups[category]:
|
|
822
|
+
print(f" {acc.id:3d}. {acc.issuer} - {acc.account}")
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _handle_get(password: str, account_id: int):
|
|
826
|
+
"""子命令 get:用 ID 获取验证码
|
|
827
|
+
|
|
828
|
+
零交互:密码通过参数传入,输出简洁一行式。
|
|
829
|
+
"""
|
|
830
|
+
# 初始化数据目录(确保存在)
|
|
831
|
+
init_data_dir()
|
|
832
|
+
|
|
833
|
+
db_path = str(get_db_path())
|
|
834
|
+
key_path = str(get_key_path())
|
|
835
|
+
|
|
836
|
+
# 检查主密钥文件是否存在
|
|
837
|
+
if not os.path.exists(key_path):
|
|
838
|
+
print("错误: 首次使用请先运行 mini2fa 设置主密码", file=sys.stderr)
|
|
839
|
+
sys.exit(1)
|
|
840
|
+
|
|
841
|
+
crypto = CryptoManager(key_path)
|
|
842
|
+
|
|
843
|
+
# 验证密码
|
|
844
|
+
if not crypto.initialize(password):
|
|
845
|
+
print("密码错误", file=sys.stderr)
|
|
846
|
+
sys.exit(1)
|
|
847
|
+
|
|
848
|
+
storage = StorageManager(db_path, crypto)
|
|
849
|
+
account = storage.get_account(account_id)
|
|
850
|
+
|
|
851
|
+
if not account:
|
|
852
|
+
print(f"账号不存在: {account_id}", file=sys.stderr)
|
|
853
|
+
sys.exit(1)
|
|
854
|
+
|
|
855
|
+
# 解密密钥并生成验证码
|
|
856
|
+
secret = storage.get_secret(account_id)
|
|
857
|
+
code = generate_totp(secret, account.algorithm, account.digits, account.period)
|
|
858
|
+
remaining = get_remaining_seconds(account.period)
|
|
859
|
+
|
|
860
|
+
# 简洁一行式:issuer - account: code (remaining)
|
|
861
|
+
print(f"{account.issuer} - {account.account}: {code} ({remaining}s)")
|
|
862
|
+
|
|
863
|
+
|
|
729
864
|
def _main_inner():
|
|
730
865
|
"""程序主体(不含 TUI 生命周期)"""
|
|
731
866
|
ui.clear() # 启动:清屏后显示 banner,保证顶部干净
|
|
@@ -768,6 +903,8 @@ def _main_inner():
|
|
|
768
903
|
else:
|
|
769
904
|
ui.print_line(f"\n✗ 主密码设置失败:已尝试 {MAX_PASSWORD_ATTEMPTS} 次。")
|
|
770
905
|
ui.print_line(" 请重新运行 mini2fa 重新设置。")
|
|
906
|
+
import time
|
|
907
|
+
time.sleep(1.5)
|
|
771
908
|
sys.exit(1)
|
|
772
909
|
|
|
773
910
|
# 设置密保提示
|
|
@@ -777,6 +914,8 @@ def _main_inner():
|
|
|
777
914
|
ui.print_line("✓ 主密码已设置成功!")
|
|
778
915
|
else:
|
|
779
916
|
ui.print_line("✗ 初始化失败!")
|
|
917
|
+
import time
|
|
918
|
+
time.sleep(1.5)
|
|
780
919
|
sys.exit(1)
|
|
781
920
|
else:
|
|
782
921
|
# 获取密保提示
|
|
@@ -794,7 +933,9 @@ def _main_inner():
|
|
|
794
933
|
if hint:
|
|
795
934
|
ui.print_line(f"💡 密码提示:{hint}")
|
|
796
935
|
else:
|
|
797
|
-
ui.print_line("✗
|
|
936
|
+
ui.print_line("✗ 密码错误次数过多,程序将退出")
|
|
937
|
+
import time
|
|
938
|
+
time.sleep(1.5)
|
|
798
939
|
sys.exit(1)
|
|
799
940
|
|
|
800
941
|
# 初始化存储管理器
|
|
@@ -846,6 +987,8 @@ def _run():
|
|
|
846
987
|
except StorageCorruptedError as e:
|
|
847
988
|
ui.print_line(f"\n✗ {e}")
|
|
848
989
|
ui.print_line(" 请检查数据文件是否完整,或从备份恢复。")
|
|
990
|
+
import time
|
|
991
|
+
time.sleep(1.5)
|
|
849
992
|
sys.exit(1)
|
|
850
993
|
|
|
851
994
|
|
|
@@ -197,7 +197,8 @@ class TestChangePasswordCLI:
|
|
|
197
197
|
# getpass 3次都是错误旧密码
|
|
198
198
|
inputs = iter(['wrong1', 'wrong2', 'wrong3'])
|
|
199
199
|
with patch('getpass.getpass', side_effect=lambda prompt: next(inputs)):
|
|
200
|
-
|
|
200
|
+
with patch('builtins.input', return_value=''):
|
|
201
|
+
handle_change_password(crypto)
|
|
201
202
|
|
|
202
203
|
from mini2fa.crypto import CryptoManager
|
|
203
204
|
reload = CryptoManager(key)
|
|
@@ -318,7 +319,8 @@ class TestAddAccountFlow:
|
|
|
318
319
|
storage.add_account('Google', 'u@gmail.com', 'JBSWY3DPEHPK3PXP')
|
|
319
320
|
try:
|
|
320
321
|
# 若流程错误进入填字段, next() 会 StopIteration 崩溃
|
|
321
|
-
|
|
322
|
+
# 需要两个 input: 图片路径 + 按 Enter 返回主菜单
|
|
323
|
+
inputs = iter(['fake.png', ''])
|
|
322
324
|
with patch('mini2fa._cli.os.path.exists', return_value=True):
|
|
323
325
|
with patch('mini2fa._cli.scan_qrcode', return_value=info):
|
|
324
326
|
with patch('builtins.input', side_effect=lambda prompt: next(inputs)):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|