Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Extract EULAs from a .dmg file #8231

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/cask/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 8 additions & 0 deletions lib/cask/container/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ def initialize(cask, path, command)
@path = path
@command = command
end

def eula?
!eulas.empty?
end

def eulas
[]
end
end
14 changes: 12 additions & 2 deletions lib/cask/container/dmg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def initialize(*args)
@mounts = []
end

def eulas
@eulas ||= Cask::Container::DmgEula.all(realpath)
end

def extract
mount!
assert_mounts_found
Expand All @@ -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)
Expand Down Expand Up @@ -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
99 changes: 99 additions & 0 deletions lib/cask/container/dmg_eula.rb
Original file line number Diff line number Diff line change
@@ -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<hdiutil udifderez -rez "#{ dmg_filename }" | awk -F\\" '/^data .#{ format }. \\(#{ language_id }[,)]/{current=1} /^\\t\\$/ {if(current) {print $2}} /};/{current=0}' | xxd -r -p | iconv -f MAC -t UTF-8 | tr '\\r' '\\n' | textutil -stdin -format #{ file_extension } -convert txt -stdout>
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<hdiutil udifderez -rez "#{ dmg_filename }" | iconv -f MAC -t UTF-8 | sed -n -E "s/^data ('TEXT'|'RTF ') \\((.*)\\) {/- :format: \\1\\\\\n :language: [ \\2 ]/p" | awk 'BEGIN { print "---" } 1'>
end
end
end
24 changes: 23 additions & 1 deletion lib/cask/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def install(force=false)
begin
satisfy_dependencies
download
display_eula
extract_primary_container
install_artifacts
save_caskfile force
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/cask/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down
145 changes: 145 additions & 0 deletions lib/cask/utils/locale.rb
Original file line number Diff line number Diff line change
@@ -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