teams-cli 0.2.4__tar.gz → 0.2.6__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.
Files changed (74) hide show
  1. {teams_cli-0.2.4 → teams_cli-0.2.6}/CHANGELOG.md +18 -0
  2. {teams_cli-0.2.4 → teams_cli-0.2.6}/Cargo.lock +167 -6
  3. {teams_cli-0.2.4 → teams_cli-0.2.6}/Cargo.toml +5 -3
  4. {teams_cli-0.2.4 → teams_cli-0.2.6}/PKG-INFO +11 -1
  5. {teams_cli-0.2.4 → teams_cli-0.2.6}/README.md +10 -0
  6. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/install.rs +216 -27
  7. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/macos.rs +33 -5
  8. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop.js +32 -7
  9. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop.rs +4 -3
  10. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop_cache.js +9 -2
  11. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop_people.js +34 -27
  12. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/tui.rs +91 -2
  13. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/desktop_scripts.test.cjs +67 -7
  14. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/js/people.test.mjs +2 -1
  15. {teams_cli-0.2.4 → teams_cli-0.2.6}/.github/workflows/ci.yml +0 -0
  16. {teams_cli-0.2.4 → teams_cli-0.2.6}/.github/workflows/dependencies.yml +0 -0
  17. {teams_cli-0.2.4 → teams_cli-0.2.6}/.github/workflows/release.yml +0 -0
  18. {teams_cli-0.2.4 → teams_cli-0.2.6}/.gitignore +0 -0
  19. {teams_cli-0.2.4 → teams_cli-0.2.6}/.mise.toml +0 -0
  20. {teams_cli-0.2.4 → teams_cli-0.2.6}/LICENSE +0 -0
  21. {teams_cli-0.2.4 → teams_cli-0.2.6}/Makefile +0 -0
  22. {teams_cli-0.2.4 → teams_cli-0.2.6}/build.rs +0 -0
  23. {teams_cli-0.2.4 → teams_cli-0.2.6}/docs/releases.md +0 -0
  24. {teams_cli-0.2.4 → teams_cli-0.2.6}/examples/validate_schema.rs +0 -0
  25. {teams_cli-0.2.4 → teams_cli-0.2.6}/pyproject.toml +0 -0
  26. {teams_cli-0.2.4 → teams_cli-0.2.6}/scripts/smoke-release.py +0 -0
  27. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/auth/pass.rs +0 -0
  28. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/auth/process.rs +0 -0
  29. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/auth/wsl.rs +0 -0
  30. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/auth/wsl_credentials.ps1 +0 -0
  31. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/auth.rs +0 -0
  32. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/backend.rs +0 -0
  33. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/cli.rs +0 -0
  34. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/config.rs +0 -0
  35. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/delivery.rs +0 -0
  36. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/avatars.rs +0 -0
  37. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser.rs +0 -0
  38. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser_children.rs +0 -0
  39. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser_core.rs +0 -0
  40. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser_install.rs +0 -0
  41. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser_lifetime.rs +0 -0
  42. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/browser_setup.rs +0 -0
  43. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/cdp.rs +0 -0
  44. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/connection.ps1 +0 -0
  45. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/connection.rs +0 -0
  46. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/helper.rs +0 -0
  47. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/owner.rs +0 -0
  48. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/session.rs +0 -0
  49. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop/windows.rs +0 -0
  50. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop_core.js +0 -0
  51. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/desktop_send.js +0 -0
  52. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/error.rs +0 -0
  53. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/graph.rs +0 -0
  54. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/lib.rs +0 -0
  55. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/main.rs +0 -0
  56. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/output.rs +0 -0
  57. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/render.rs +0 -0
  58. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/schema.rs +0 -0
  59. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/setup/capabilities.rs +0 -0
  60. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/setup.rs +0 -0
  61. {teams_cli-0.2.4 → teams_cli-0.2.6}/src/teams_cli_alias.rs +0 -0
  62. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/browser_lifetime.rs +0 -0
  63. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/cli.rs +0 -0
  64. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/connection.rs +0 -0
  65. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/connection.test.ps1 +0 -0
  66. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/credentials.rs +0 -0
  67. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/desktop.rs +0 -0
  68. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/desktop_scripts.rs +0 -0
  69. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/fixtures/clispec-v0.3.json +0 -0
  70. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/package_alias.rs +0 -0
  71. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/pass_credentials.sh +0 -0
  72. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/release_smoke.rs +0 -0
  73. {teams_cli-0.2.4 → teams_cli-0.2.6}/tests/wsl_credentials.test.ps1 +0 -0
  74. {teams_cli-0.2.4 → teams_cli-0.2.6}/vership.toml +0 -0
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.6](https://github.com/rvben/teams-cli/compare/v0.2.5...v0.2.6) - 2026-10-27
11
+
12
+ ### Fixed
13
+
14
+ - `teams people search` finds work and school colleagues again with the desktop backend. It now uses the same directory search as the Teams people picker; the lookup it used before returned nothing, or failed with "Teams could not resolve this person" in a headless browser.
15
+ - **desktop**: accept signed macOS system UIDs ([b7a1521](https://github.com/rvben/teams-cli/commit/b7a152189d2bc636cebe5ca67f24172dd702ae75))
16
+ - **desktop**: validate helper archives with tar-codec ([b955dd8](https://github.com/rvben/teams-cli/commit/b955dd8ea93cb302d72107cd2701e2adc8380ab3))
17
+
18
+ ## [0.2.5](https://github.com/rvben/teams-cli/compare/v0.2.4...v0.2.5) - 2026-10-21
19
+
20
+ ### Fixed
21
+
22
+ - Update TLS certificate verification and bundled root certificates to address RUSTSEC-2026-0382 and RUSTSEC-2026-0383.
23
+ - Desktop chats whose messages Teams keeps in a secondary reply-chain store (`replychains-2`, ...) no longer look empty. All such stores are now read and merged, which also fills in member names for those 1:1 chats.
24
+ - Desktop chat messages missing from the current cache are filled in from caches the same user and tenant left under earlier display languages. Those caches no longer update, so messages edited or deleted since can appear as they were.
25
+ - In the TUI, a chat whose messages are missing now shows its latest message from the chat list and why, instead of "No messages in this chat yet." With the desktop backend it says no messages for the chat were found in the Teams cache on this PC.
26
+ - **deps**: update TLS certificate trust dependencies ([435b11c](https://github.com/rvben/teams-cli/commit/435b11c39113e165e843d73579bc28505ef05620))
27
+
10
28
  ## [0.2.4](https://github.com/rvben/teams-cli/compare/v0.2.3...v0.2.4) - 2026-10-20
11
29
 
12
30
  ### Fixed
@@ -37,6 +37,12 @@ version = "0.2.21"
37
37
  source = "registry+https://github.com/rust-lang/crates.io-index"
38
38
  checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
39
39
 
40
+ [[package]]
41
+ name = "ambient-authority"
42
+ version = "0.0.2"
43
+ source = "registry+https://github.com/rust-lang/crates.io-index"
44
+ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
45
+
40
46
  [[package]]
41
47
  name = "anstream"
42
48
  version = "1.0.0"
@@ -102,6 +108,19 @@ dependencies = [
102
108
  "num-traits",
103
109
  ]
104
110
 
111
+ [[package]]
112
+ name = "archive-trait"
113
+ version = "0.0.15"
114
+ source = "registry+https://github.com/rust-lang/crates.io-index"
115
+ checksum = "999bf88cc483664cab77700ca2fdfb0415a73412abe284ef5bc2319d00938e00"
116
+ dependencies = [
117
+ "cap-std",
118
+ "thiserror 2.0.20",
119
+ "tokio",
120
+ "walkdir",
121
+ "windows-sys 0.60.2",
122
+ ]
123
+
105
124
  [[package]]
106
125
  name = "assert-json-diff"
107
126
  version = "2.0.2"
@@ -127,6 +146,18 @@ dependencies = [
127
146
  "wait-timeout",
128
147
  ]
129
148
 
149
+ [[package]]
150
+ name = "async-compression"
151
+ version = "0.4.51"
152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
153
+ checksum = "9d5755640120b24961f8231e05892e4a4f7e0d04e8aeca161508c9c397946970"
154
+ dependencies = [
155
+ "compression-codecs",
156
+ "compression-core",
157
+ "pin-project-lite",
158
+ "tokio",
159
+ ]
160
+
130
161
  [[package]]
131
162
  name = "atomic"
132
163
  version = "0.6.1"
@@ -296,6 +327,36 @@ version = "1.12.1"
296
327
  source = "registry+https://github.com/rust-lang/crates.io-index"
297
328
  checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
298
329
 
330
+ [[package]]
331
+ name = "cap-primitives"
332
+ version = "4.0.3"
333
+ source = "registry+https://github.com/rust-lang/crates.io-index"
334
+ checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18"
335
+ dependencies = [
336
+ "ambient-authority",
337
+ "fs-set-times",
338
+ "io-extras",
339
+ "io-lifetimes 3.0.1",
340
+ "ipnet",
341
+ "maybe-owned",
342
+ "rustix",
343
+ "rustix-linux-procfs",
344
+ "windows-sys 0.61.2",
345
+ "winx",
346
+ ]
347
+
348
+ [[package]]
349
+ name = "cap-std"
350
+ version = "4.0.3"
351
+ source = "registry+https://github.com/rust-lang/crates.io-index"
352
+ checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8"
353
+ dependencies = [
354
+ "cap-primitives",
355
+ "io-extras",
356
+ "io-lifetimes 3.0.1",
357
+ "rustix",
358
+ ]
359
+
299
360
  [[package]]
300
361
  name = "castaway"
301
362
  version = "0.2.4"
@@ -428,6 +489,23 @@ dependencies = [
428
489
  "static_assertions",
429
490
  ]
430
491
 
492
+ [[package]]
493
+ name = "compression-codecs"
494
+ version = "0.4.46"
495
+ source = "registry+https://github.com/rust-lang/crates.io-index"
496
+ checksum = "cae36683101b2899f48bd31f8db9839437356188e3b1ff23096e73dab09ba8fe"
497
+ dependencies = [
498
+ "compression-core",
499
+ "flate2",
500
+ "memchr",
501
+ ]
502
+
503
+ [[package]]
504
+ name = "compression-core"
505
+ version = "0.4.34"
506
+ source = "registry+https://github.com/rust-lang/crates.io-index"
507
+ checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a"
508
+
431
509
  [[package]]
432
510
  name = "const-oid"
433
511
  version = "0.10.2"
@@ -959,6 +1037,17 @@ dependencies = [
959
1037
  "num",
960
1038
  ]
961
1039
 
1040
+ [[package]]
1041
+ name = "fs-set-times"
1042
+ version = "0.20.3"
1043
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1044
+ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
1045
+ dependencies = [
1046
+ "io-lifetimes 2.0.4",
1047
+ "rustix",
1048
+ "windows-sys 0.52.0",
1049
+ ]
1050
+
962
1051
  [[package]]
963
1052
  name = "fs_extra"
964
1053
  version = "1.3.0"
@@ -1419,6 +1508,28 @@ dependencies = [
1419
1508
  "syn 3.0.4",
1420
1509
  ]
1421
1510
 
1511
+ [[package]]
1512
+ name = "io-extras"
1513
+ version = "0.19.0"
1514
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1515
+ checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f"
1516
+ dependencies = [
1517
+ "io-lifetimes 3.0.1",
1518
+ "windows-sys 0.60.2",
1519
+ ]
1520
+
1521
+ [[package]]
1522
+ name = "io-lifetimes"
1523
+ version = "2.0.4"
1524
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1525
+ checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
1526
+
1527
+ [[package]]
1528
+ name = "io-lifetimes"
1529
+ version = "3.0.1"
1530
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1531
+ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96"
1532
+
1422
1533
  [[package]]
1423
1534
  name = "ipnet"
1424
1535
  version = "2.12.1"
@@ -1780,6 +1891,12 @@ dependencies = [
1780
1891
  "winapi",
1781
1892
  ]
1782
1893
 
1894
+ [[package]]
1895
+ name = "maybe-owned"
1896
+ version = "0.3.4"
1897
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1898
+ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
1899
+
1783
1900
  [[package]]
1784
1901
  name = "memchr"
1785
1902
  version = "2.8.3"
@@ -2750,6 +2867,16 @@ dependencies = [
2750
2867
  "windows-sys 0.61.2",
2751
2868
  ]
2752
2869
 
2870
+ [[package]]
2871
+ name = "rustix-linux-procfs"
2872
+ version = "0.1.1"
2873
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2874
+ checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
2875
+ dependencies = [
2876
+ "once_cell",
2877
+ "rustix",
2878
+ ]
2879
+
2753
2880
  [[package]]
2754
2881
  name = "rustls"
2755
2882
  version = "0.23.46"
@@ -2815,9 +2942,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
2815
2942
 
2816
2943
  [[package]]
2817
2944
  name = "rustls-webpki"
2818
- version = "0.103.15"
2945
+ version = "0.103.16"
2819
2946
  source = "registry+https://github.com/rust-lang/crates.io-index"
2820
- checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
2947
+ checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
2821
2948
  dependencies = [
2822
2949
  "aws-lc-rs",
2823
2950
  "ring",
@@ -3185,11 +3312,34 @@ dependencies = [
3185
3312
  "xattr",
3186
3313
  ]
3187
3314
 
3315
+ [[package]]
3316
+ name = "tar-codec"
3317
+ version = "0.0.15"
3318
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3319
+ checksum = "b9dad615e50d181afea4bafea23dacb8e33ed52dde06503813570634b932d467"
3320
+ dependencies = [
3321
+ "archive-trait",
3322
+ "tar-framing",
3323
+ "thiserror 2.0.20",
3324
+ "tokio",
3325
+ ]
3326
+
3327
+ [[package]]
3328
+ name = "tar-framing"
3329
+ version = "0.0.15"
3330
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3331
+ checksum = "563e8181b47c9cdcce569eae3ad431eec71543d6269dd60c7bd2350f70bcd233"
3332
+ dependencies = [
3333
+ "thiserror 2.0.20",
3334
+ "tokio",
3335
+ ]
3336
+
3188
3337
  [[package]]
3189
3338
  name = "teams-cli"
3190
- version = "0.2.4"
3339
+ version = "0.2.6"
3191
3340
  dependencies = [
3192
3341
  "assert_cmd",
3342
+ "async-compression",
3193
3343
  "base64 0.23.1",
3194
3344
  "clap",
3195
3345
  "clap_complete",
@@ -3210,6 +3360,7 @@ dependencies = [
3210
3360
  "serde_json",
3211
3361
  "sha2",
3212
3362
  "tar",
3363
+ "tar-codec",
3213
3364
  "tempfile",
3214
3365
  "thiserror 2.0.20",
3215
3366
  "tokio",
@@ -3229,7 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
3229
3380
  checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
3230
3381
  dependencies = [
3231
3382
  "fastrand",
3232
- "getrandom 0.3.4",
3383
+ "getrandom 0.4.3",
3233
3384
  "once_cell",
3234
3385
  "rustix",
3235
3386
  "windows-sys 0.61.2",
@@ -3843,9 +3994,9 @@ dependencies = [
3843
3994
 
3844
3995
  [[package]]
3845
3996
  name = "webpki-root-certs"
3846
- version = "1.0.9"
3997
+ version = "1.0.10"
3847
3998
  source = "registry+https://github.com/rust-lang/crates.io-index"
3848
- checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
3999
+ checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
3849
4000
  dependencies = [
3850
4001
  "rustls-pki-types",
3851
4002
  ]
@@ -4121,6 +4272,16 @@ version = "1.0.4"
4121
4272
  source = "registry+https://github.com/rust-lang/crates.io-index"
4122
4273
  checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
4123
4274
 
4275
+ [[package]]
4276
+ name = "winx"
4277
+ version = "0.36.4"
4278
+ source = "registry+https://github.com/rust-lang/crates.io-index"
4279
+ checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
4280
+ dependencies = [
4281
+ "bitflags 2.13.1",
4282
+ "windows-sys 0.52.0",
4283
+ ]
4284
+
4124
4285
  [[package]]
4125
4286
  name = "wiremock"
4126
4287
  version = "0.6.5"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "teams-cli"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  edition = "2024"
5
5
  rust-version = "1.90"
6
6
  description = "A Microsoft Teams CLI and TUI designed equally for humans and agents"
@@ -22,12 +22,12 @@ name = "teams-cli"
22
22
  path = "src/teams_cli_alias.rs"
23
23
 
24
24
  [dependencies]
25
+ async-compression = { version = "0.4", features = ["tokio", "gzip"] }
25
26
  base64 = "0.23"
26
27
  clap = { version = "4", features = ["derive", "env"] }
27
28
  clap_complete = "4"
28
29
  crossterm = "0.29"
29
30
  dirs = "6"
30
- flate2 = "1"
31
31
  futures-util = { version = "0.3", default-features = false, features = ["sink"] }
32
32
  keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "vendored"] }
33
33
  open = "5"
@@ -37,7 +37,7 @@ reqwest = { version = "0.13", default-features = false, features = ["rustls", "j
37
37
  serde = { version = "1", features = ["derive"] }
38
38
  serde_json = "1"
39
39
  sha2 = "0.10"
40
- tar = "0.4"
40
+ tar-codec = "=0.0.15"
41
41
  thiserror = "2"
42
42
  tempfile = "3"
43
43
  tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "io-util", "time", "process", "io-std"] }
@@ -64,6 +64,8 @@ windows-sys = { version = "0.61", features = [
64
64
  libc = "0.2"
65
65
 
66
66
  [dev-dependencies]
67
+ flate2 = "1"
68
+ tar = "0.4"
67
69
  assert_cmd = "2"
68
70
  jsonschema = "0.49.8"
69
71
  predicates = "3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teams-cli
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  License-File: LICENSE
@@ -24,6 +24,16 @@ For humans, no arguments open guided onboarding on first use and the TUI afterwa
24
24
 
25
25
  ## Install
26
26
 
27
+ ### Homebrew
28
+
29
+ ```sh
30
+ brew install rvben/tap/teams-cli
31
+ ```
32
+
33
+ Homebrew installs native binaries on macOS and builds from source on Linux.
34
+
35
+ ### Other installation methods
36
+
27
37
  ```console
28
38
  pipx install teams-cli
29
39
  # or
@@ -8,6 +8,16 @@ For humans, no arguments open guided onboarding on first use and the TUI afterwa
8
8
 
9
9
  ## Install
10
10
 
11
+ ### Homebrew
12
+
13
+ ```sh
14
+ brew install rvben/tap/teams-cli
15
+ ```
16
+
17
+ Homebrew installs native binaries on macOS and builds from source on Linux.
18
+
19
+ ### Other installation methods
20
+
11
21
  ```console
12
22
  pipx install teams-cli
13
23
  # or
@@ -3,10 +3,11 @@
3
3
  use crate::error::AppError;
4
4
  use sha2::{Digest, Sha256};
5
5
  use std::{
6
- io::Read,
7
6
  path::{Path, PathBuf},
8
7
  time::Duration,
9
8
  };
9
+ use tar_codec::{Archive as _, Member, MemberPayload as _, TarArchive};
10
+ use tokio::io::AsyncReadExt as _;
10
11
 
11
12
  use super::helper::PROTOCOL;
12
13
 
@@ -133,7 +134,15 @@ async fn ensure_state(state: HelperState, version: &str) -> Result<PathBuf, AppE
133
134
  "{asset} does not match its published checksum; nothing was installed"
134
135
  )));
135
136
  }
136
- install(&path, &extract(&archive)?)?;
137
+ // Member payloads borrow the decoder across awaits. Parse on a blocking
138
+ // worker so the downloader remains Send on Rust 1.90, and decompression
139
+ // does not occupy an async runtime worker.
140
+ let helper = tokio::task::spawn_blocking(move || {
141
+ tokio::runtime::Handle::current().block_on(extract(&archive))
142
+ })
143
+ .await
144
+ .map_err(|_| AppError::Desktop("could not read the helper archive".into()))??;
145
+ install(&path, &helper)?;
137
146
  Ok(path)
138
147
  }
139
148
 
@@ -192,30 +201,73 @@ fn checksum(sums: &str, name: &str) -> Option<String> {
192
201
  })
193
202
  }
194
203
 
195
- /// Only `teams.exe` is taken from the archive.
196
- fn extract(archive: &[u8]) -> Result<Vec<u8>, AppError> {
204
+ /// Only one top-level `teams.exe` is accepted. Validate the entire archive
205
+ /// before installing it, including entries after the executable and gzip CRC.
206
+ async fn extract(archive: &[u8]) -> Result<Vec<u8>, AppError> {
197
207
  let invalid = || AppError::Desktop("the helper archive is not valid".into());
198
- let mut entries = tar::Archive::new(flate2::read::GzDecoder::new(archive));
199
- for entry in entries.entries().map_err(|_| invalid())? {
200
- let entry = entry.map_err(|_| invalid())?;
201
- let is_helper = entry.header().entry_type().is_file()
202
- && entry.path().ok().is_some_and(|p| {
203
- p.components().filter(|c| c.as_os_str() != ".").count() == 1
204
- && p.file_name().is_some_and(|n| n == "teams.exe")
205
- });
206
- if is_helper {
207
- let mut bytes = Vec::new();
208
- entry
209
- .take(MAX_ARCHIVE as u64 + 1)
210
- .read_to_end(&mut bytes)
211
- .map_err(|_| invalid())?;
212
- if bytes.len() > MAX_ARCHIVE || !bytes.starts_with(b"MZ") {
208
+ if archive.len() > MAX_ARCHIVE {
209
+ return Err(invalid());
210
+ }
211
+ let mut reader =
212
+ async_compression::tokio::bufread::GzipDecoder::new(archive).take(MAX_ARCHIVE as u64 + 1);
213
+ let mut helper = None;
214
+ {
215
+ let mut entries = TarArchive::new(&mut reader).members();
216
+ let mut chunk = Vec::new();
217
+ while let Some(entry) = entries.next().await.map_err(|_| invalid())? {
218
+ if let Member::File {
219
+ metadata,
220
+ size,
221
+ mut payload,
222
+ ..
223
+ } = entry
224
+ {
225
+ // Match the release layout without normalizing away traversal.
226
+ let is_helper = matches!(metadata.path.as_str(), "teams.exe" | "./teams.exe");
227
+ if is_helper {
228
+ if helper.is_some() || size > MAX_ARCHIVE as u64 {
229
+ return Err(invalid());
230
+ }
231
+ let mut bytes = Vec::new();
232
+ while payload
233
+ .next_chunk(&mut chunk, 64 * 1024)
234
+ .await
235
+ .map_err(|_| invalid())?
236
+ {
237
+ if bytes.len() + chunk.len() > MAX_ARCHIVE {
238
+ return Err(invalid());
239
+ }
240
+ bytes.extend_from_slice(&chunk);
241
+ }
242
+ if !bytes.starts_with(b"MZ") {
243
+ return Err(invalid());
244
+ }
245
+ helper = Some(bytes);
246
+ } else {
247
+ payload.skip().await.map_err(|_| invalid())?;
248
+ }
249
+ } else if !matches!(entry, Member::Directory { .. }) {
250
+ // Release packages contain only directories and regular files.
213
251
  return Err(invalid());
214
252
  }
215
- return Ok(bytes);
216
253
  }
217
254
  }
218
- Err(invalid())
255
+ // The tar end marker can precede padding and the gzip trailer. Reading to
256
+ // EOF also makes the decompressed-size budget cover ignored archive data.
257
+ let mut padding = [0; 8192];
258
+ loop {
259
+ let read = reader.read(&mut padding).await.map_err(|_| invalid())?;
260
+ if read == 0 {
261
+ break;
262
+ }
263
+ if padding[..read].iter().any(|&byte| byte != 0) {
264
+ return Err(invalid());
265
+ }
266
+ }
267
+ if reader.limit() == 0 || !reader.get_ref().get_ref().is_empty() {
268
+ return Err(invalid());
269
+ }
270
+ helper.ok_or_else(invalid)
219
271
  }
220
272
 
221
273
  /// Writes atomically, marks it executable for WSL interop, and removes other versions.
@@ -249,6 +301,12 @@ fn install(path: &Path, bytes: &[u8]) -> Result<(), AppError> {
249
301
  mod tests {
250
302
  use super::*;
251
303
 
304
+ #[test]
305
+ fn helper_downloader_can_be_sent_between_runtime_workers() {
306
+ fn assert_send(_: impl Send) {}
307
+ assert_send(ensure());
308
+ }
309
+
252
310
  #[tokio::test]
253
311
  async fn legacy_release_needs_a_built_helper_but_accepts_explicit_or_cached_builds() {
254
312
  let temp = tempfile::tempdir().unwrap();
@@ -299,14 +357,145 @@ mod tests {
299
357
  assert_eq!(hex(&[0, 15, 255]), "000fff");
300
358
  }
301
359
 
302
- #[test]
303
- fn extracts_only_the_top_level_windows_executable() {
360
+ #[tokio::test]
361
+ async fn extracts_only_the_top_level_windows_executable() {
304
362
  let exe = b"MZ fixture";
305
363
  let bytes = archive(&[("./README.md", b"readme"), ("./teams.exe", exe)]);
306
- assert_eq!(extract(&bytes).unwrap(), exe);
307
- assert!(extract(&archive(&[("./nested/teams.exe", exe)])).is_err());
308
- assert!(extract(&archive(&[("./teams.exe", b"#!/bin/sh")])).is_err());
309
- assert!(extract(b"not an archive").is_err());
364
+ assert_eq!(extract(&bytes).await.unwrap(), exe);
365
+ assert!(
366
+ extract(&archive(&[("./nested/teams.exe", exe)]))
367
+ .await
368
+ .is_err()
369
+ );
370
+ assert!(
371
+ extract(&archive(&[("./teams.exe", b"#!/bin/sh")]))
372
+ .await
373
+ .is_err()
374
+ );
375
+ assert!(extract(b"not an archive").await.is_err());
376
+ }
377
+
378
+ #[tokio::test]
379
+ async fn extracts_a_helper_across_multiple_payload_chunks() {
380
+ let mut exe = vec![0x5a; 3 * 64 * 1024 + 17];
381
+ exe[..2].copy_from_slice(b"MZ");
382
+ assert_eq!(
383
+ extract(&archive(&[("teams.exe", &exe)])).await.unwrap(),
384
+ exe
385
+ );
386
+ }
387
+
388
+ #[cfg(unix)]
389
+ #[tokio::test]
390
+ async fn accepts_the_release_workflows_system_tar_packaging() {
391
+ let temp = tempfile::tempdir().unwrap();
392
+ let package = temp.path().join("package");
393
+ std::fs::create_dir(&package).unwrap();
394
+ std::fs::write(package.join("teams.exe"), b"MZ release fixture").unwrap();
395
+ std::fs::write(package.join("README.md"), b"readme").unwrap();
396
+ let output = temp.path().join("helper.tar.gz");
397
+ // Match .github/workflows/release.yml, including the '.' root entry.
398
+ assert!(
399
+ std::process::Command::new("tar")
400
+ .arg("-C")
401
+ .arg(&package)
402
+ .arg("-czf")
403
+ .arg(&output)
404
+ .arg(".")
405
+ .status()
406
+ .unwrap()
407
+ .success()
408
+ );
409
+ assert_eq!(
410
+ extract(&std::fs::read(output).unwrap()).await.unwrap(),
411
+ b"MZ release fixture"
412
+ );
413
+ }
414
+
415
+ #[tokio::test]
416
+ async fn rejects_duplicate_helpers_and_damage_after_the_helper() {
417
+ let duplicate = archive(&[("teams.exe", b"MZ first"), ("./teams.exe", b"MZ second")]);
418
+ assert!(extract(&duplicate).await.is_err());
419
+
420
+ let mut damaged = archive(&[("teams.exe", b"MZ fixture")]);
421
+ let crc = damaged.len() - 8;
422
+ damaged[crc] ^= 1;
423
+ assert!(extract(&damaged).await.is_err(), "gzip CRC must be checked");
424
+ damaged.truncate(crc);
425
+ assert!(
426
+ extract(&damaged).await.is_err(),
427
+ "missing trailer must fail"
428
+ );
429
+
430
+ // A malformed second tar header must not be hidden by finding the
431
+ // executable first. Recompress so the gzip checksum remains valid.
432
+ use std::io::{Read as _, Write as _};
433
+ let valid = archive(&[("teams.exe", b"MZ fixture"), ("README.md", b"readme")]);
434
+ let mut raw = Vec::new();
435
+ flate2::read::GzDecoder::new(valid.as_slice())
436
+ .read_to_end(&mut raw)
437
+ .unwrap();
438
+ raw[1024] ^= 1;
439
+ let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
440
+ gz.write_all(&raw).unwrap();
441
+ assert!(extract(&gz.finish().unwrap()).await.is_err());
442
+
443
+ let mut trailing = archive(&[("teams.exe", b"MZ fixture")]);
444
+ trailing.extend_from_slice(b"junk");
445
+ assert!(
446
+ extract(&trailing).await.is_err(),
447
+ "trailing compressed data must fail"
448
+ );
449
+
450
+ let mut concatenated = archive(&[("teams.exe", b"MZ first")]);
451
+ concatenated.extend_from_slice(&archive(&[("teams.exe", b"MZ second")]));
452
+ assert!(
453
+ extract(&concatenated).await.is_err(),
454
+ "multiple gzip streams must fail"
455
+ );
456
+ }
457
+
458
+ #[tokio::test]
459
+ async fn rejects_links_even_after_a_valid_helper() {
460
+ let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
461
+ Vec::new(),
462
+ flate2::Compression::fast(),
463
+ ));
464
+ let mut file = tar::Header::new_gnu();
465
+ file.set_size(2);
466
+ file.set_mode(0o644);
467
+ builder
468
+ .append_data(&mut file, "teams.exe", &b"MZ"[..])
469
+ .unwrap();
470
+ let mut link = tar::Header::new_gnu();
471
+ link.set_entry_type(tar::EntryType::Symlink);
472
+ link.set_size(0);
473
+ link.set_mode(0o777);
474
+ builder
475
+ .append_link(&mut link, "other", "teams.exe")
476
+ .unwrap();
477
+ let bytes = builder.into_inner().unwrap().finish().unwrap();
478
+ assert!(extract(&bytes).await.is_err());
479
+ }
480
+
481
+ #[tokio::test]
482
+ async fn bounds_decompressed_data_including_ignored_files_and_padding() {
483
+ // Compressed input is small, but ignored payloads still consume the
484
+ // same total budget as the helper itself.
485
+ let bytes = archive(&[("README.md", &vec![0; MAX_ARCHIVE]), ("teams.exe", b"MZ")]);
486
+ assert!(bytes.len() < MAX_ARCHIVE);
487
+ assert!(extract(&bytes).await.is_err());
488
+
489
+ use std::io::{Read as _, Write as _};
490
+ let valid = archive(&[("teams.exe", b"MZ")]);
491
+ let mut raw = Vec::new();
492
+ flate2::read::GzDecoder::new(valid.as_slice())
493
+ .read_to_end(&mut raw)
494
+ .unwrap();
495
+ raw.resize(MAX_ARCHIVE + 1, 0);
496
+ let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
497
+ gz.write_all(&raw).unwrap();
498
+ assert!(extract(&gz.finish().unwrap()).await.is_err());
310
499
  }
311
500
 
312
501
  #[test]