diff --git a/Library/Homebrew/cask/quarantine.rb b/Library/Homebrew/cask/quarantine.rb index d55f775f02f6c..bf2b79728d608 100644 --- a/Library/Homebrew/cask/quarantine.rb +++ b/Library/Homebrew/cask/quarantine.rb @@ -20,7 +20,6 @@ class SigningIdentity < T::Struct QUARANTINE_ATTRIBUTE = "com.apple.quarantine" USER_APPROVED_FLAG = 0x0040 - QUARANTINE_SCRIPT = T.let((HOMEBREW_LIBRARY_PATH/"cask/utils/quarantine.swift").freeze, Pathname) COPY_XATTRS_SCRIPT = T.let((HOMEBREW_LIBRARY_PATH/"cask/utils/copy-xattrs.swift").freeze, Pathname) sig { returns(T.nilable(T.any(String, Pathname))) } @@ -70,7 +69,7 @@ def self.check_quarantine_support raise "unexpected nil swift" unless s api_check = system_command(s, - args: [*swift_target_args, QUARANTINE_SCRIPT], + args: [*swift_target_args, COPY_XATTRS_SCRIPT], print_stderr: false) exit_status = api_check.exit_status @@ -205,26 +204,40 @@ def self.cask!(cask: nil, download_path: nil, action: true) odebug "Quarantining #{download_path}" - raise "unexpected nil swift" unless swift + require "os/mac/ffi" - quarantiner = system_command(T.must(swift), - args: [ - *swift_target_args, - QUARANTINE_SCRIPT, - download_path, - cask.url.to_s, - cask.homepage.to_s, - ], - print_stderr: false) + path_cf_string = MacOS::FFI::CoreFoundation.string_create(download_path.to_s) + raise CaskQuarantineError.new(download_path, "Failed to create CFString for path") if path_cf_string.null? - return if quarantiner.success? + path_cf_url = MacOS::FFI::CoreFoundation.url_create_with_file_system_path(path_cf_string) + raise CaskQuarantineError.new(download_path, "Failed to create CFURL for path") if path_cf_url.null? - case quarantiner.exit_status - when 2 - raise CaskQuarantineError.new(download_path, "Insufficient parameters") - else - raise CaskQuarantineError.new(download_path, quarantiner.stderr) + quarantine_agent_name = MacOS::FFI::CoreFoundation.string_create("Homebrew Cask") + quarantine_data_url = MacOS::FFI::CoreFoundation.string_create(cask.url.to_s) + quarantine_origin_url = MacOS::FFI::CoreFoundation.string_create(cask.homepage.to_s) + if quarantine_agent_name.null? || quarantine_data_url.null? || quarantine_origin_url.null? + raise CaskQuarantineError.new(download_path, "Failed to create CFString for quarantine properties") end + + quarantine_dictionary = MacOS::FFI::CoreFoundation.dictionary_create( + MacOS::FFI::LaunchServices.quarantine_agent_name_key => quarantine_agent_name, + MacOS::FFI::LaunchServices.quarantine_type_key => MacOS::FFI::LaunchServices.quarantine_type_web_download, + MacOS::FFI::LaunchServices.quarantine_data_url_key => quarantine_data_url, + MacOS::FFI::LaunchServices.quarantine_origin_url_key => quarantine_origin_url, + ) + if quarantine_dictionary.null? + raise CaskQuarantineError.new(download_path, "Failed to create quarantine dictionary") + end + + success = MacOS::FFI::CoreFoundation.url_set_resource_property_for_key( + path_cf_url, + MacOS::FFI::CoreFoundation.url_quarantine_properties_key, + quarantine_dictionary, + ) + + return if success + + raise CaskQuarantineError.new(download_path, "Failed to set quarantine properties for URL") end sig { params(from: T.nilable(Pathname), to: T.nilable(Pathname)).void } diff --git a/Library/Homebrew/cask/utils/quarantine.swift b/Library/Homebrew/cask/utils/quarantine.swift deleted file mode 100755 index 55c6721ef11ee..0000000000000 --- a/Library/Homebrew/cask/utils/quarantine.swift +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/swift - -import Foundation - -struct SwiftErr: TextOutputStream { - public static var stream = SwiftErr() - - mutating func write(_ string: String) { - fputs(string, stderr) - } -} - -guard CommandLine.arguments.count >= 4 else { - print("Usage: swift quarantine.swift ") - exit(2) -} - -var dataLocationURL = URL(fileURLWithPath: CommandLine.arguments[1]) - -let quarantineProperties: [String: Any] = [ - kLSQuarantineAgentNameKey as String: "Homebrew Cask", - kLSQuarantineTypeKey as String: kLSQuarantineTypeWebDownload, - kLSQuarantineDataURLKey as String: CommandLine.arguments[2], - kLSQuarantineOriginURLKey as String: CommandLine.arguments[3] -] - -// Check for if the data location URL is reachable -do { - let isDataLocationURLReachable = try dataLocationURL.checkResourceIsReachable() - guard isDataLocationURLReachable else { - print("URL \(dataLocationURL.path) is not reachable. Not proceeding.", to: &SwiftErr.stream) - exit(1) - } -} catch { - print(error.localizedDescription, to: &SwiftErr.stream) - exit(1) -} - -// Quarantine the file -do { - var resourceValues = URLResourceValues() - resourceValues.quarantineProperties = quarantineProperties - try dataLocationURL.setResourceValues(resourceValues) -} catch { - print(error.localizedDescription, to: &SwiftErr.stream) - exit(1) -} diff --git a/Library/Homebrew/extend/os/mac/linkage_checker.rb b/Library/Homebrew/extend/os/mac/linkage_checker.rb index 6d8ecb26fb5d6..50bdb4d818798 100644 --- a/Library/Homebrew/extend/os/mac/linkage_checker.rb +++ b/Library/Homebrew/extend/os/mac/linkage_checker.rb @@ -4,28 +4,18 @@ module OS module Mac module LinkageChecker - private + extend T::Helpers - sig { returns(T::Boolean) } - def system_libraries_exist_in_cache? - # In macOS Big Sur and later, system libraries do not exist on-disk and instead exist in a cache. - MacOS.version >= :big_sur - end + requires_ancestor { ::LinkageChecker } + + private sig { params(dylib: String).returns(T::Boolean) } def dylib_found_in_shared_cache?(dylib) - Kernel.require "fiddle" - @dyld_shared_cache_contains_path ||= T.let(begin - libc = Fiddle.dlopen("/usr/lib/libSystem.B.dylib") - - Fiddle::Function.new( - libc["_dyld_shared_cache_contains_path"], - [Fiddle::TYPE_CONST_STRING], - Fiddle::TYPE_BOOL, - ) - end, T.nilable(Fiddle::Function)) + return false if MacOS.version < :big_sur - @dyld_shared_cache_contains_path.call(dylib) + require "os/mac/ffi" + MacOS::FFI.dyld_shared_cache_contains_path(dylib) end end end diff --git a/Library/Homebrew/linkage_checker.rb b/Library/Homebrew/linkage_checker.rb index 6e08488d1d3d7..ad24d2792bac9 100644 --- a/Library/Homebrew/linkage_checker.rb +++ b/Library/Homebrew/linkage_checker.rb @@ -213,9 +213,8 @@ def check_dylibs(rebuild_cache:) if (dep = dylib_to_dep(dylib)) broken_dep = (@broken_deps[dep] ||= []) broken_dep << dylib unless broken_dep.include?(dylib) - elsif system_libraries_exist_in_cache? && dylib_found_in_shared_cache?(dylib) - # If we cannot associate the dylib with a dependency, then it may be a system library. - # Check the dylib shared cache for the library to verify this. + elsif dylib_found_in_shared_cache?(dylib) + # In macOS Big Sur and later, system libraries do not exist on-disk and instead exist in a cache. @system_dylibs << dylib elsif !system_framework?(dylib) && !broken_dylibs_allowed?(file.to_s) @broken_dylibs << dylib @@ -242,11 +241,6 @@ def check_dylibs(rebuild_cache:) store.update!(keg_files_dylibs:) end - sig { returns(T::Boolean) } - def system_libraries_exist_in_cache? - false - end - sig { params(_dylib: String).returns(T::Boolean) } def dylib_found_in_shared_cache?(_dylib) false diff --git a/Library/Homebrew/os/mac/ffi.rb b/Library/Homebrew/os/mac/ffi.rb new file mode 100644 index 0000000000000..d6e04d255a405 --- /dev/null +++ b/Library/Homebrew/os/mac/ffi.rb @@ -0,0 +1,208 @@ +# typed: strict +# frozen_string_literal: true + +require "fiddle" + +module OS + module Mac + # Wrapping module for FFI calls to system libraries. + module FFI + # NativeLibrary provides helper methods for loading system libraries and accessing functions and constants. + # Functions and constants are cached so they only need to be looked up once. + module NativeLibrary + private + + sig { params(path: String).void } + def use_library(path) + @library_path = T.let(path.freeze, T.nilable(String)) + end + + sig { returns(Fiddle::Handle) } + def handle + @handle ||= T.let(Fiddle.dlopen(T.must(@library_path)), T.nilable(Fiddle::Handle)) + end + + sig { params(name: String, argument_types: T::Array[Integer], return_type: Integer).returns(Fiddle::Function) } + def function(name, argument_types, return_type) + @functions ||= T.let({}, T.nilable(T::Hash[String, Fiddle::Function])) + @functions[name] ||= Fiddle::Function.new(handle[name], argument_types, return_type) + end + + sig { params(name: String, dereference: T::Boolean).returns(Fiddle::Pointer) } + def constant(name, dereference: false) + @constants ||= T.let({}, T.nilable(T::Hash[[String, T::Boolean], Fiddle::Pointer])) + @constants[[name, dereference]] ||= begin + pointer = Fiddle::Pointer.new(handle[name]) + dereference ? pointer.ptr : pointer + end + end + end + + extend NativeLibrary + + use_library "/usr/lib/libSystem.B.dylib" + + # mach-o/dyld.h: + # bool _dyld_shared_cache_contains_path(const char* path); + sig { params(path: String).returns(T::Boolean) } + def self.dyld_shared_cache_contains_path(path) + function( + "_dyld_shared_cache_contains_path", + [Fiddle::TYPE_CONST_STRING], + Fiddle::TYPE_BOOL, + ).call(path) + end + + # CoreFoundation.framework wrapper + module CoreFoundation + extend NativeLibrary + + use_library "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation" + + sig { params(ptr: Fiddle::Pointer).returns(Fiddle::Pointer) } + private_class_method def self.autorelease(ptr) + return ptr if ptr.null? + + # CoreFoundation/CFBase.h: + # void CFRelease(CFTypeRef cf); + ptr.free = function("CFRelease", [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID) + ptr + end + + # CoreFoundation/CFDictionary.h: + # extern const CFDictionaryKeyCallBacks kCFTypeDictionaryKeyCallBacks; + sig { returns(Fiddle::Pointer) } + def self.type_dictionary_key_call_backs = constant("kCFTypeDictionaryKeyCallBacks") + + # CoreFoundation/CFDictionary.h: + # extern const CFDictionaryValueCallBacks kCFTypeDictionaryValueCallBacks + sig { returns(Fiddle::Pointer) } + def self.type_dictionary_value_call_backs = constant("kCFTypeDictionaryValueCallBacks") + + # CoreFoundation/CFURL.h: + # extern const CFStringRef kCFURLQuarantinePropertiesKey; + sig { returns(Fiddle::Pointer) } + def self.url_quarantine_properties_key = constant("kCFURLQuarantinePropertiesKey", dereference: true) + + # CoreFoundation/CFString.h: + # CFStringRef CFStringCreateWithCString(CFAllocatorRef alloc, const char *cStr, CFStringEncoding encoding); + sig { params(string: String).returns(Fiddle::Pointer) } + def self.string_create(string) + cf_encoding = case string.encoding + when Encoding::UTF_8 + 0x08000100 # kCFStringEncodingUTF8 + when Encoding::US_ASCII + 0x0600 # kCFStringEncodingASCII + when Encoding::ASCII_8BIT, Encoding::ISO8859_1 + # ASCII-8BIT could be anything, so just use Latin-1 + 0x0201 # kCFStringEncodingISOLatin1 + else + # Try convert to UTF-8 and move on + string = string.encode(Encoding::UTF_8) + 0x08000100 + end + + autorelease( + function( + "CFStringCreateWithCString", + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_CONST_STRING, Fiddle::TYPE_UINT32_T], + Fiddle::TYPE_VOIDP, + ).call(nil, string, cf_encoding), + ) + end + + # CoreFoundation/CFDictionary.h: + # CFDictionaryRef CFDictionaryCreate( + # CFAllocatorRef allocator, + # const void **keys, + # const void **values, + # CFIndex numValues, + # const CFDictionaryKeyCallBacks *keyCallBacks, + # const CFDictionaryValueCallBacks *valueCallBacks); + sig { params(hash: T::Hash[Fiddle::Pointer, Fiddle::Pointer]).returns(Fiddle::Pointer) } + def self.dictionary_create(hash) + size = Fiddle::SIZEOF_VOIDP * hash.size + Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) do |keys| + Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE) do |values| + # Convert array of pointers to continous stream of pointers in the C buffer + keys[0, size] = hash.keys.pack("J*") + values[0, size] = hash.values.pack("J*") + return autorelease( + function( + "CFDictionaryCreate", + [ + Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, + Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP + ], + Fiddle::TYPE_VOIDP, + ).call( + nil, keys, values, hash.size, type_dictionary_key_call_backs, type_dictionary_value_call_backs + ), + ) + end + end + end + + # CoreFoundation/CFURL.h: + # CFURLRef CFURLCreateWithFileSystemPath(CFAllocatorRef allocator, + # CFStringRef filePath, CFURLPathStyle pathStyle, Boolean isDirectory); + sig { params(path: Fiddle::Pointer).returns(Fiddle::Pointer) } + def self.url_create_with_file_system_path(path) + autorelease( + function( + "CFURLCreateWithFileSystemPath", + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_BOOL], + Fiddle::TYPE_VOIDP, + ).call(nil, path, 0, false), + ) + end + + # CoreFoundation/CFURL.h: + # Boolean CFURLSetResourcePropertyForKey(CFURLRef url, CFStringRef key, CFTypeRef value, CFErrorRef *error); + sig { params(url: Fiddle::Pointer, key: Fiddle::Pointer, value: Fiddle::Pointer).returns(T::Boolean) } + def self.url_set_resource_property_for_key(url, key, value) + function( + "CFURLSetResourcePropertyForKey", + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_BOOL, + ).call(url, key, value, nil) + end + end + + # LaunchServices.framework wrapper + module LaunchServices + extend NativeLibrary + + use_library( + "/System/Library/Frameworks/CoreServices.framework/Versions/A/" \ + "Frameworks/LaunchServices.framework/Versions/A/LaunchServices", + ) + + # LaunchServices/LSQuarantine.h: + # extern const CFStringRef kLSQuarantineAgentNameKey; + sig { returns(Fiddle::Pointer) } + def self.quarantine_agent_name_key = constant("kLSQuarantineAgentNameKey", dereference: true) + + # LaunchServices/LSQuarantine.h: + # extern const CFStringRef kLSQuarantineTypeKey; + sig { returns(Fiddle::Pointer) } + def self.quarantine_type_key = constant("kLSQuarantineTypeKey", dereference: true) + + # LaunchServices/LSQuarantine.h: + # extern const CFStringRef kLSQuarantineTypeWebDownload; + sig { returns(Fiddle::Pointer) } + def self.quarantine_type_web_download = constant("kLSQuarantineTypeWebDownload", dereference: true) + + # LaunchServices/LSQuarantine.h: + # extern const CFStringRef kLSQuarantineDataURLKey; + sig { returns(Fiddle::Pointer) } + def self.quarantine_data_url_key = constant("kLSQuarantineDataURLKey", dereference: true) + + # LaunchServices/LSQuarantine.h: + # extern const CFStringRef kLSQuarantineOriginURLKey; + sig { returns(Fiddle::Pointer) } + def self.quarantine_origin_url_key = constant("kLSQuarantineOriginURLKey", dereference: true) + end + end + end +end diff --git a/Library/Homebrew/sorbet/rbi/shims/rspec.rbi b/Library/Homebrew/sorbet/rbi/shims/rspec.rbi index 620eea9d3ec49..5895899748609 100644 --- a/Library/Homebrew/sorbet/rbi/shims/rspec.rbi +++ b/Library/Homebrew/sorbet/rbi/shims/rspec.rbi @@ -4,6 +4,7 @@ class RSpec::Core::ExampleGroup include RSpec::SharedContext include RSpec::Matchers include RSpec::Mocks::ExampleMethods + include Test::Helper::MkTmpDir end # The rspec-mocks RBI defines `ExpectHost#expect(target)` with a required diff --git a/Library/Homebrew/test/cask/quarantine_spec.rb b/Library/Homebrew/test/cask/quarantine_spec.rb index 7fb5c51c6f35a..015e2df28b32c 100644 --- a/Library/Homebrew/test/cask/quarantine_spec.rb +++ b/Library/Homebrew/test/cask/quarantine_spec.rb @@ -4,6 +4,41 @@ RSpec.describe Cask::Quarantine do let(:klass) { Cask::Quarantine } + describe ".cask!", :needs_macos do + let(:cask) do + instance_double( + Cask::Cask, + url: "https://example.com/download", + homepage: "https://example.com", + ) + end + + it "sets the quarantine attribute on a file in a temporary directory" do + mktmpdir do |tmpdir| + download_path = tmpdir/"Test.dmg" + download_path.write("test") + + expect(klass.status(download_path)).to eq("") + + klass.cask!(cask:, download_path:) + + expect(klass.status(download_path)).to match( + /\A[0-9a-f]{4};[0-9a-f]+;Homebrew\\x20Cask;[0-9A-F-]{36}\z/i, + ) + end + end + + it "raises when the quarantine properties cannot be written" do + mktmpdir do |tmpdir| + download_path = tmpdir/"missing.dmg" + + expect do + klass.cask!(cask:, download_path:) + end.to raise_error(Cask::CaskQuarantineError, /Failed to set quarantine properties for URL/) + end + end + end + describe ".user_approved?" do let(:file) { Pathname("/tmp/Test.app") } diff --git a/Library/Homebrew/test/support/helper/mktmpdir.rb b/Library/Homebrew/test/support/helper/mktmpdir.rb index f6cc4282c9ee7..8e64a223d8bc6 100644 --- a/Library/Homebrew/test/support/helper/mktmpdir.rb +++ b/Library/Homebrew/test/support/helper/mktmpdir.rb @@ -1,12 +1,20 @@ -# typed: false +# typed: strict # frozen_string_literal: true module Test module Helper module MkTmpDir - def mktmpdir(prefix_suffix = nil) + extend T::Sig + + sig do + params( + prefix_suffix: T.nilable(T.any(String, T::Array[String])), + blk: T.nilable(T.proc.params(tmpdir: Pathname).returns(T.untyped)), + ).returns(T.untyped) + end + def mktmpdir(prefix_suffix = nil, &blk) new_dir = Pathname.new(Dir.mktmpdir(prefix_suffix, HOMEBREW_TEMP)) - return yield new_dir if block_given? + return blk.call(new_dir) if blk new_dir end