瀏覽代碼

fix(cli): handle symlinks in updater bundler, closes #3933 (#3934)

Co-authored-by: chip <chip@chip.sh>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
Dave Ceddia 2 年之前
父節點
當前提交
ef962c43af

+ 15 - 64
core/tauri/src/api/file/extract.rs

@@ -6,7 +6,7 @@ use std::{
   borrow::Cow,
   fs,
   io::{self, Cursor, Read, Seek},
-  path::{self, Path, PathBuf},
+  path::{self, Component, Path, PathBuf},
 };
 
 /// The archive reader.
@@ -84,10 +84,22 @@ impl<'a, R: Read> Entry<'a, R> {
 
   /// Extract this entry into `into_path`.
   /// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
+  /// If it's a symlink, it will be created.
   /// Note: You need to include the complete path, with file name and extension.
   pub fn extract(self, into_path: &path::Path) -> crate::api::Result<()> {
     match self {
       Self::Tar(mut entry) => {
+        // validate path
+        let path = entry.path()?;
+        if path.components().any(|c| matches!(c, Component::ParentDir)) {
+          return Err(
+            std::io::Error::new(
+              std::io::ErrorKind::InvalidInput,
+              "cannot extract path with parent dir component",
+            )
+            .into(),
+          );
+        }
         // determine if it's a file or a directory
         if entry.header().entry_type() == tar::EntryType::Directory {
           // this is a directory, lets create it
@@ -100,13 +112,8 @@ impl<'a, R: Read> Entry<'a, R> {
             }
           }
         } else {
-          let mut out_file = fs::File::create(into_path)?;
-          io::copy(&mut entry, &mut out_file)?;
-
-          // make sure we set permissions
-          if let Ok(mode) = entry.header().mode() {
-            set_perms(into_path, Some(&mut out_file), mode, true)?;
-          }
+          // handle files, symlinks, hard links, etc. and set permissions
+          entry.unpack(into_path)?;
         }
       }
       Self::Zip(entry) => {
@@ -270,59 +277,3 @@ impl<'a, R: Read + Seek> Extract<'a, R> {
     Ok(())
   }
 }
-
-fn set_perms(
-  dst: &Path,
-  f: Option<&mut std::fs::File>,
-  mode: u32,
-  preserve: bool,
-) -> crate::api::Result<()> {
-  _set_perms(dst, f, mode, preserve).map_err(|_| {
-    crate::api::Error::Extract(format!(
-      "failed to set permissions to {mode:o} \
-               for `{}`",
-      dst.display()
-    ))
-  })
-}
-
-#[cfg(unix)]
-fn _set_perms(
-  dst: &Path,
-  f: Option<&mut std::fs::File>,
-  mode: u32,
-  preserve: bool,
-) -> io::Result<()> {
-  use std::os::unix::prelude::*;
-
-  let mode = if preserve { mode } else { mode & 0o777 };
-  let perm = fs::Permissions::from_mode(mode as _);
-  match f {
-    Some(f) => f.set_permissions(perm),
-    None => fs::set_permissions(dst, perm),
-  }
-}
-
-#[cfg(windows)]
-fn _set_perms(
-  dst: &Path,
-  f: Option<&mut std::fs::File>,
-  mode: u32,
-  _preserve: bool,
-) -> io::Result<()> {
-  if mode & 0o200 == 0o200 {
-    return Ok(());
-  }
-  match f {
-    Some(f) => {
-      let mut perm = f.metadata()?.permissions();
-      perm.set_readonly(true);
-      f.set_permissions(perm)
-    }
-    None => {
-      let mut perm = fs::metadata(dst)?.permissions();
-      perm.set_readonly(true);
-      fs::set_permissions(dst, perm)
-    }
-  }
-}

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/Headers

@@ -0,0 +1 @@
+Versions/Current/Headers

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/Resources

@@ -0,0 +1 @@
+Versions/Current/Resources

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/Versions/A/Headers/test.h

@@ -0,0 +1 @@
+// Testing that a header can be included

+ 32 - 0
core/tests/app-updater/frameworks/test.framework/Versions/A/Resources/Info.plist

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>test</string>
+	<key>CFBundleIdentifier</key>
+	<string>com.tauri.test.framework</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>test</string>
+	<key>CFBundlePackageType</key>
+	<string>FMWK</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0.0</string>
+	<key>CFBundleVersion</key>
+	<string>1.0.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>10.15</string>
+	<key>CFBundleSupportedPlatforms</key>
+	<array>
+		<string>MacOSX</string>
+	</array>
+	<key>NSPrincipalClass</key>
+	<string></string>
+</dict>
+</plist>

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/Versions/A/Resources/LICENSE

@@ -0,0 +1 @@
+Test that a LICENSE file can be included

+ 3 - 0
core/tests/app-updater/frameworks/test.framework/Versions/A/test

@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+
+echo "hello"

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/Versions/Current

@@ -0,0 +1 @@
+A

+ 1 - 0
core/tests/app-updater/frameworks/test.framework/test

@@ -0,0 +1 @@
+Versions/Current/test

+ 5 - 0
core/tests/app-updater/tauri.conf.json

@@ -17,6 +17,11 @@
         "../../../examples/.icons/icon.ico"
       ],
       "category": "DeveloperTool",
+      "macOS": {
+        "frameworks": [
+          "./frameworks/test.framework"
+        ]
+      },
       "windows": {
         "wix": {
           "skipWebviewInstall": true

+ 17 - 0
core/tests/app-updater/tests/update.rs

@@ -310,6 +310,23 @@ fn update_app() {
 
     let status = binary_cmd.status().expect("failed to run app");
 
+    // Verify the framework extracted symlinks correctly
+    #[cfg(target_os = "macos")]
+    {
+      let meta = std::fs::symlink_metadata(
+        bundle_paths(&root_dir, "0.1.0")
+          .first()
+          .unwrap()
+          .1
+          .join("Contents/Frameworks/test.framework/test"),
+      )
+      .expect("test.framework/test metadata");
+      assert!(
+        meta.file_type().is_symlink(),
+        "test.framework/test should be a symlink"
+      );
+    }
+
     if !status.success() {
       panic!("failed to run app");
     }

+ 11 - 1
tooling/bundler/src/bundle/updater_bundle.rs

@@ -238,12 +238,22 @@ fn create_tar(src_dir: &Path, dest_path: &Path) -> crate::Result<PathBuf> {
   let gzip_encoder = libflate::gzip::Encoder::new(dest_file)?;
 
   let gzip_encoder = create_tar_from_src(src_dir, gzip_encoder)?;
+
   let mut dest_file = gzip_encoder.finish().into_result()?;
   dest_file.flush()?;
   Ok(dest_path.to_owned())
 }
 
-#[cfg(not(target_os = "windows"))]
+#[cfg(target_os = "macos")]
+fn create_tar_from_src<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
+  let src_dir = src_dir.as_ref();
+  let mut builder = tar::Builder::new(dest_file);
+  builder.follow_symlinks(false);
+  builder.append_dir_all(src_dir.file_name().expect("Path has no file_name"), src_dir)?;
+  builder.into_inner().map_err(Into::into)
+}
+
+#[cfg(target_os = "linux")]
 fn create_tar_from_src<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
   let src_dir = src_dir.as_ref();
   let mut tar_builder = tar::Builder::new(dest_file);