From 859a18667809727f3f99d961b00c6ce2a11f04db Mon Sep 17 00:00:00 2001 From: Claudia Date: Thu, 18 Dec 2014 13:16:55 +0100 Subject: [PATCH] Installer: Extract EULAs from a .dmg file; proof-of-concept --- lib/cask/container.rb | 1 + lib/cask/container/base.rb | 8 ++ lib/cask/container/dmg.rb | 14 +++- lib/cask/container/dmg_eula.rb | 99 ++++++++++++++++++++++ lib/cask/installer.rb | 24 +++++- lib/cask/utils.rb | 2 + lib/cask/utils/locale.rb | 145 +++++++++++++++++++++++++++++++++ 7 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 lib/cask/container/dmg_eula.rb create mode 100644 lib/cask/utils/locale.rb diff --git a/lib/cask/container.rb b/lib/cask/container.rb index e04613b842fb1..169de26d214ec 100644 --- a/lib/cask/container.rb +++ b/lib/cask/container.rb @@ -6,6 +6,7 @@ class Cask::Container; end require 'cask/container/cab' require 'cask/container/criteria' require 'cask/container/dmg' +require 'cask/container/dmg_eula' require 'cask/container/generic_unar' require 'cask/container/gzip' require 'cask/container/naked' diff --git a/lib/cask/container/base.rb b/lib/cask/container/base.rb index 969ea195a911d..ce9f6bcb75163 100644 --- a/lib/cask/container/base.rb +++ b/lib/cask/container/base.rb @@ -4,4 +4,12 @@ def initialize(cask, path, command) @path = path @command = command end + + def eula? + !eulas.empty? + end + + def eulas + [] + end end diff --git a/lib/cask/container/dmg.rb b/lib/cask/container/dmg.rb index 2110c334f71be..65c68825e63a9 100644 --- a/lib/cask/container/dmg.rb +++ b/lib/cask/container/dmg.rb @@ -9,6 +9,10 @@ def initialize(*args) @mounts = [] end + def eulas + @eulas ||= Cask::Container::DmgEula.all(realpath) + end + def extract mount! assert_mounts_found @@ -34,8 +38,7 @@ def mount! plist = @command.run('/usr/bin/hdiutil', # :startup may not be the minimum necessary privileges :bsexec => :startup, - # realpath is a failsafe against unusual filenames - :args => %w[mount -plist -nobrowse -readonly -noidme -mountrandom /tmp] + [Pathname.new(@path).realpath], + :args => %w[mount -plist -nobrowse -readonly -noidme -mountrandom /tmp] + [realpath], :input => %w[y] ).plist @mounts = mounts_from_plist(plist) @@ -75,4 +78,11 @@ def eject! raise CaskError.new "Failed to eject #{mountpath}" end end + + private + + def realpath + # realpath is a failsafe against unusual filenames + Pathname.new(@path).realpath + end end diff --git a/lib/cask/container/dmg_eula.rb b/lib/cask/container/dmg_eula.rb new file mode 100644 index 0000000000000..cbde76284ad40 --- /dev/null +++ b/lib/cask/container/dmg_eula.rb @@ -0,0 +1,99 @@ +class Cask::Container::DmgEula + attr_reader :dmg_filename, :format, :language_id, :language_name + + LANGUAGE_RANK = lambda do |eula| + region_code = eula.mac_system_region_code + locale = Cask::Utils::Locale.locale + locale_match = locale.match_system_region?(region_code) + + [ + (locale_match ? 0 : 1), # Prefer exact matches + eula.language_id, # If inexact, prefer lower IDs (0 is English) + (eula.rich_text? ? 0 : 1) # Also, prefer rich text over plain text + ] + end + + def self.all(dmg_filename) + EULASpecParser.new(dmg_filename) + .parse + .sort_by(&LANGUAGE_RANK) + end + + def show!(title) + less_prompt = [ + %Q[License agreement for "#{ title }"], + 'Page %dm ?Bof %D.', + '[H]elp [Q]uit' + ].join(' ') + + system("#{ content_command } | fmt | less -P '#{ less_prompt }'") + end + + def content + @content ||= `#{ content_command }` + end + + def mac_system_region_code + language_id - Cask::Utils::Locale.resource_fork_region_base_id + end + + def plain_text? + format === 'TEXT' + end + + def rich_text? + format === 'RTF ' + end + + def to_s + "#{ language_name } language EULA (id: #{ language_id }, format: #{ file_extension })" + end + + def initialize(dmg_filename, format, language_id, language_name) + @dmg_filename = dmg_filename + @format = format + @language_id = language_id + @language_name = language_name + end + + private + + def content_command + %Q + end + + def file_extension + case + when plain_text? then 'txt' + when rich_text? then 'rtf' + end + end + + class EULASpecParser + attr_reader :dmg_filename + + def initialize(dmg_filename) + @dmg_filename = dmg_filename + end + + def parse + eula_spec = YAML::load(eula_spec_yaml) || [] + eula_spec.map do |raw_spec| + odebug "EULA spec parsed from DMG: #{ raw_spec }" + language_id, language_name = raw_spec[:language] + Cask::Container::DmgEula.new(dmg_filename, + raw_spec[:format], language_id, language_name) + end + end + + private + + def eula_spec_yaml + `#{ eula_spec_yaml_command }` + end + + def eula_spec_yaml_command + %Q + end + end +end diff --git a/lib/cask/installer.rb b/lib/cask/installer.rb index 632b03f336a4c..d2d517440566f 100644 --- a/lib/cask/installer.rb +++ b/lib/cask/installer.rb @@ -57,6 +57,7 @@ def install(force=false) begin satisfy_dependencies download + display_eula extract_primary_container install_artifacts save_caskfile force @@ -86,8 +87,29 @@ def download @downloaded_path end + def display_eula + unless primary_container.eula? + odebug "No EULA found" + return + end + eula = primary_container.eulas.first + odebug "Selecting #{ eula }" + odebug "Mac system region code: #{ eula.mac_system_region_code }" + odebug "EULA source format: #{ eula.format }" + + eula.show!(@cask.token) + end + def extract_primary_container odebug "Extracting primary container" + primary_container.extract + end + + def primary_container + @primary_container ||= load_primary_container + end + + def load_primary_container FileUtils.mkdir_p @cask.staged_path container = if @cask.container and @cask.container.type Cask::Container.from_type(@cask.container.type) @@ -98,7 +120,7 @@ def extract_primary_container raise CaskError.new "Uh oh, could not identify primary container for '#{@downloaded_path}'" end odebug "Using container class #{container} for #{@downloaded_path}" - container.new(@cask, @downloaded_path, @command).extract + container.new(@cask, @downloaded_path, @command) end def install_artifacts diff --git a/lib/cask/utils.rb b/lib/cask/utils.rb index c3a674f172c48..902b61cb35f30 100644 --- a/lib/cask/utils.rb +++ b/lib/cask/utils.rb @@ -89,6 +89,8 @@ def odebug title, *sput end module Cask::Utils + require 'cask/utils/locale' + def dumpcask if Cask.respond_to?(:debug) and Cask.debug odebug "Cask instance dumps in YAML:" diff --git a/lib/cask/utils/locale.rb b/lib/cask/utils/locale.rb new file mode 100644 index 0000000000000..8079b947c9349 --- /dev/null +++ b/lib/cask/utils/locale.rb @@ -0,0 +1,145 @@ +require 'yaml' + +module Cask::Utils::Locale + GLOBAL_PREFERENCES_DOMAIN = '.GlobalPreferences' + APPLE_LOCALE_KEY = "AppleLocale" + + RESOURCE_FORK_REGION_BASE_ID = 5000 + + # Sources: + # + # - /System/Library/Frameworks/CoreServices.framework/Frameworks/ \ + # CarbonCore.framework/Headers/Script.h + # + # - http://whitefiles.org/b1_s/1_free_guides/fg3mo/pgs/t02.htm + # + # - http://www.filibeto.org/unix/macos/lib/dev/documentation/mac/ \ + # pdf/Text/Script_Manager.pdf + + OS_LOCALES = YAML::load(<<-EOF.undent).freeze + --- + 0: 'en_US' # United States + 1: 'fr_FR' # France + 2: 'en_GB' # Britain + 3: 'de_DE' # Germany + 4: 'it_IT' # Italy + 5: 'nl_NL' # Netherlands + 6: 'nl_BE' # Flemish (Dutch) for Belgium + 7: 'sv_SE' # Sweden + 8: 'es_ES' # Spanish for Spain + 9: 'da_DK' # Denmark + 10: 'pt_PT' # Portuguese for Portugal + 11: 'fr_CA' # French for Canada + 12: 'nb_NO' # Bokmål + 13: 'he_IL' # Hebrew + 14: 'ja_JP' # Japan + 15: 'en_AU' # English for Australia + 16: 'ar' # Arabic + 17: 'fi_FI' # Finland + 18: 'fr_CH' # French Swiss + 19: 'de_CH' # Swiss German + 20: 'el_GR' # German Swiss + 21: 'is_IS' # Iceland + 22: 'mt_MT' # Malta + 23: 'el_CY' # Cyprus + 24: 'tr_TR' # Turkey + 25: 'hr_HR' # Yugo/Croatian + 33: 'hi_IN' # India/Hindi + 34: 'ur_PK' # Pakistan/Urdu + 35: 'tr_TR' # Turkish (Modified) + 36: 'it_CH' # Italian Swiss + 37: 'en-ascii' # English for international use + 39: 'ro_RO' # Romania + 40: 'grc' # Ancient Greek + 41: 'lt_LT' # Lithuania + 42: 'pl_PL' # Poland + 43: 'hu_HU' # Hungary + 44: 'et_EE' # Estonia + 45: 'lv_LV' # Latvia + 46: 'se' # Lapland + 47: 'fo_FO' # Faroe Islands + 48: 'fa_IR' # Iran + 49: 'ru_RU' # Russia + 50: 'ga_IE' # Ireland + 51: 'ko_KR' # Korea + 52: 'zh_CN' # China + 53: 'zh_TW' # Taiwan + 54: 'th_TH' # Thailand + 56: 'cs_CZ' # Czech + 57: 'sk_SK' # Slovak + 60: 'bn' # Bangladesh or India + 61: 'be_BY' # Belarus + 62: 'uk_UA' # Ukraine + 65: 'sr_CS' # Serbian + 66: 'sl_SI' # Slovenian + 67: 'mk_MK' # Macedonian + 68: 'hr_HR' # Croatia + 70: 'de-1996' # German (reformed) + 71: 'pt_BR' # Portuguese for Brazil + 72: 'bg_BG' # Bulgaria + 73: 'ca_ES' # Catalonia + 75: 'gd' # Scottish Gaelic + 76: 'gv' # Isle of Man + 77: 'br' # Breton + 78: 'iu_CA' # Inuktitut for Canada + 79: 'cy' # Welsh + 81: 'ga-Latg_IE' # Irish Gaelic for Ireland + 82: 'en_CA' # English for Canada + 83: 'dz_BT' # Dzongkha for Bhutan + 84: 'hy_AM' # Armenian + 85: 'ka_GE' # Georgian + 86: 'es_XL' # Spanish for Latin America + 88: 'to_TO' # Tonga + 91: 'fr' # French generic + 92: 'de_AT' # German for Austria + 94: 'gu_IN' # Gujarati + 95: 'pa' # Pakistan or India + 96: 'ur_IN' # Urdu for India + 97: 'vi_VN' # Vietnam + 98: 'fr_BE' # French for Belgium + 99: 'uz_UZ' # Uzbek + 100: 'en_SG' # Singapore + 101: 'nn_NO' # Norwegian Nynorsk + 102: 'af_ZA' # Afrikaans + 103: 'eo' # Esperanto + 104: 'mr_IN' # Marathi + 105: 'bo' # Tibetan + 106: 'ne_NP' # Nepal + 107: 'kl' # Greenland + 108: 'en_IE' # English for Ireland, with Euro for currency + EOF + + Locale = Struct.new(:locale_string) do + def match_system_region?(region_code) + os_locale_string = OS_LOCALES[region_code] + + if os_locale_string + os_locale_string === locale_string.to_s + else + false + end + end + + def to_s + locale_string + end + end + + def self.locale + @@locale ||= load_locale + end + + def self.resource_fork_region_base_id + RESOURCE_FORK_REGION_BASE_ID + end + + def self.load_locale + options = { + :args => ['read', GLOBAL_PREFERENCES_DOMAIN, APPLE_LOCALE_KEY] + } + locale_string = Cask::SystemCommand.run('/usr/bin/defaults', options) + locale = Locale.new(locale_string.to_s.chomp) + odebug "Current OS locale is: #{ locale }" + locale + end +end