Validate content-length of downloaded extension tar gz files (cherry-pick #10430) (#10441)

gcp-cherry-pick-bot[bot] , Max Brunsfeld , and Marshall created

Cherry-picked Validate content-length of downloaded extension tar gz
files (#10430)

Release Notes:

- Fixed a bug where extension installation would appear to succeed even
if the download did not complete due to network interruptions
([#10330](https://github.com/zed-industries/zed/issues/10330)).

Co-authored-by: Marshall <marshall@zed.dev>

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>

Change summary

Cargo.lock                              |  1 +
crates/extension/Cargo.toml             |  1 +
crates/extension/src/extension_store.rs | 18 +++++++++++++++++-
3 files changed, 19 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -3507,6 +3507,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "isahc",
  "language",
  "log",
  "lsp",

crates/extension/Cargo.toml 🔗

@@ -23,6 +23,7 @@ collections.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+isahc.workspace = true
 language.workspace = true
 log.workspace = true
 lsp.workspace = true

crates/extension/src/extension_store.rs 🔗

@@ -605,7 +605,23 @@ impl ExtensionStore {
             )
             .await?;
 
-            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let content_length = response
+                .headers()
+                .get(isahc::http::header::CONTENT_LENGTH)
+                .and_then(|value| value.to_str().ok()?.parse::<usize>().ok());
+
+            let mut body = BufReader::new(response.body_mut());
+            let mut tgz_bytes = Vec::new();
+            body.read_to_end(&mut tgz_bytes).await?;
+
+            if let Some(content_length) = content_length {
+                let actual_len = tgz_bytes.len();
+                if content_length != actual_len {
+                    bail!("downloaded extension size {actual_len} does not match content length {content_length}");
+                }
+            }
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(tgz_bytes.as_slice()));
+            // let decompressed_bytes = GzipDecoder::new(BufReader::new(tgz_bytes));
             let archive = Archive::new(decompressed_bytes);
             archive.unpack(extension_dir).await?;
             this.update(&mut cx, |this, cx| {