Dictionary Services

0x00 前言

这篇文章是自己在学习 CFHipsterRef Chapter 13 Dictionary Services 时做的笔记。

0x01 Dictionary Services

虽然互联网广泛篡夺了它们“参考”状态,字典和词汇表在幕后扮演了重要的角色。大量的功能依赖于这些信息,从拼写检查,语法检查和自动更正到自动摘要和语义分析。

所以,作为参考,这里看看计算机在 Unix,OS X 和 iOS 中通过单词给世界带来意义的方式和方法。

0x02 Unix

几乎所有的 Unix 发行版都包括一些小的新行分隔(newline-delimited)的单词列表。在 OS X 上,这些可以在 /usr/share/dict 中找到:

1
2
3
4
5
6
7
$ ls /usr/share/dict
README
connectives
propernames
web2
web2a
words@ -> web2

符号链接 wordsweb2 单词列表,虽然不是很详尽,但仍然是一个相当大的语料库:

1
2
$ wc /usr/share/dict/words
235886 235886 2493109

这里用 head 浏览一些有趣的东西。很少如此显而易见的是这些单词都是以 “a” 开头的:

1
2
3
4
5
6
7
8
9
10
11
$ head /usr/share/dict/words
A
a
aa
aal
aalii
aam
Aani
aardvark
aardwolf
Aaron

这些巨大的,系统提供的文本文件使其很容易 grep 纵横字谜的线索(crossword puzzle clues),生成记忆密码短语和种子数据库,但从用户角度来看,/usr/share/dict 的单语主义(monolingualism)和缺乏相关的意义使它相当无用。

OS X 使用自己的系统字典构建。

0x03 OS X

/usr/share/dict 的 OS X 模拟可以在 /Library/Dictionaries 中找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ls /Library/Dictionaries/
Apple Dictionary.dictionary/
Diccionario General de la Lengua Española Vox.dictionary/
Duden Dictionary Data Set I.dictionary/
Dutch.dictionary/
Italian.dictionary/
Korean - English.dictionary/
Korean.dictionary/
Multidictionnaire de la langue francaise.dictionary/
New Oxford American Dictionary.dictionary/
Oxford American Writer's Thesaurus.dictionary/
Oxford Dictionary of English.dictionary/
Oxford Thesaurus of English.dictionary/
Sanseido Super Daijirin.dictionary/
Sanseido The WISDOM English-Japanese Japanese-English Dictionary.dictionary/
Simplified Chinese - English.dictionary/
The Standard Dictionary of Contemporary Chinese.dictionary/

OS X 附有中文,英文,法语,荷兰语,意大利语,日语和韩语的词典,以及一个英语同义词词典和一个 Apple 特定术语的特殊词典。

更深入一些,仔细看看 .dictionary bundles,看看他们真正是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ls "/Library/Dictionaries/New Oxford American Dictionary.dictionary/Contents"

Body.data
DefaultStyle.css
EntryID.data
EntryID.index
Images/
Info.plist
KeyText.data
KeyText.index
Resources/
_CodeSignature/
version.plist

一个文件系统解剖展示了一些有趣的实现细节。特别是新牛津美国词典的情况下,内容包括:

  • KeyText.dataKeyText.index & Content.data 二进制编码
  • CSS 样式条目
  • 1207 张图片,从 A-Frame 到 Zither
  • 在 US English Diacritical Pronunciation 和 International Phonetic Alphabet (IPA) 之间切换的偏好
  • 词典内容的清单 & 签名

通常,专有二进制编码意味着可以合理处理数据的结束,但是幸运的是,Core Service 提供了读取这个信息的 API。

0x0301 Gettting Definition of Word

在 OS X 上要获取一个字的定义,可以使用 DCSCopyTextDefinition 函数,在 Core Services 框架中能够找到:

1
2
3
4
5
6
7
8
#import <CoreServices/CoreServices.h>

NSString *word = @"apple";
NSString *definition =
(__bridge_transfer NSString *)DCSCopyTextDefinition(NULL,
(__bridge CFStringRef)word,
CFRangeMake(0, [word length]));
NSLog(@"%@", difinition);

等等,那些伟大的词典都去哪里了?

好吧,他们都消失在第一个 NULL 参数中。这里可以期望提供一个 DCSCopyTextDefinition 类型,如函数定义所规定的。然而,没有一个公共的函数去构造或复制这样一个类型,使 NULL 成为唯一一个可用的选项。文档也是清晰明确:

“This parameter is reserved for future use, so pass NULL. Dictionary Services searches in all active dictionaries.”

“Dictionary Services searches in all active dictionaries”,这样?听起来像是一个漏洞!

0x0302 Setting Active Dictionaries

利用漏洞来解决 Apple 平台限制的实践。一个完全易错的获取方法,比如说,同义词词典结果而不是在标准词典中第一个可用的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *dictionaryPreferences = [[userDefaults persistentDomainForName:@"com.apple.DictionaryServices"] mutableCopy];
NSArray *activeDictionaries = [dictionaryPreferences objectForKey:@"DCSActiveDictionaries"];
dictionaryPreferences[@"DCSActiveDictionaries"] = @[@"/Library/Dictionaries/Oxford American Writer's Thesaurus.dictionary"];
[userDefaults setPersistentDomain:dictionaryPreferences forName:@"com.apple.DictionaryServices"];

{
NSString *word = @"apple";
NSString *definition = (__bridge_transfer NSString *)DCSCopyTextDefinition(NULL, (__bridge CFStringRef)word, CFRangeMake(0, [word length]));
NSLog(@"%@", definition);
}

dictionaryPreferences[@"DCSActiveDictionaries"] = activeDictionaries;
[userDefaults setPersistentDomain:dictionaryPreferences forName:@"com.apple.DictionaryServices"];

“但是这是 OS X,一个平台,其宿命不能被来自 Cupertino 的贫乏的沙盒尝试(meager sandboxing attempts)所包含!”,你哭了。“难道没有一个更文明的方法吗?,比如说,私有 API?”

是的,有。

0x0303 Private APIs

没有公开地暴露,但是仍然可用通过 Core Services 使用一些函数,更接近我们渴望的词典服务(dictionary services):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extern CFArrayRef DCSCopyAvailableDictionaries();
extern CFStringRef DCSDictionaryGetName(DCSDictionaryRef dictionary);
extern CFStringRef DCSDictionaryGetShortName(DCSDictionaryRef dictionary);
extern DCSDictionaryRef DCSDictionaryCreate(CFURLRef url);
extern CFStringRef DCSDictionaryGetName(DCSDictionaryRef dictionary);
extern CFArrayRef DCSDopyRecordsForSearchString(DCSDictionaryRef dictionary, CFStringRef string, void * void *);

extern CFDictionaryRef DCSCopyDefinitionMarkup(DCSDictionaryRef dictionary, CFStringRef record);
extern CFStringRef DCSRecordCopyData(CFTypeRef record);
extern CFStringRef DCSRecordCopyDataURL(CFTypeRef record);
extern CFStringRef DCSRecordGetAnchor(CFTypeRef record);
extern CFStringRef DCSRecordGetAssociatedObj(CFTypeRef record);
extern CFStringRef DCSRecordGetHeadword(CFTypeRef record);
extern CFStringRef DCSRecordGetRawHeadword(CFTypeRef record);
extern CFStringRef DCSRecordGetString(CFTypeRef record);
extern CFStringRef DCSRecordGetTitle(CFTypeRef record);
extern DCSDictionaryRef DCSRecordGetSubDictionary(CFTypeRef record);

这些函数并没有在文档中记录,所以来看看它们是怎么使用的。

0x0304 Getting Available Dictionaries

1
2
3
4
5
6
7
8
NSMapTable *availableDictionariesKeyedByName =
[NSMapTable mapTableWithKeyOptions:NSPointerFunctionsCopyIn
valueOptions:NSPointerFunctionsObjectPointerPersonality];

for (id dictionary in (__bridge_transfer NSArray *)DCSCopyAvailableDictionaries()) {
NSString *name = (__bridge NSString *)DCSDictionaryGetName((__bridge DCSDictionaryRef)dictionary);
[availableDictionariesKeyedByName setObject:dictionary forKey:name];
}

0x0305 Getting Definition for Word

有了可用的 DCSDictionaryRef 类型示例,我们现在可以看到所有的大惊小怪都是关于 DCSCopyTextDefinition 的第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
NSString *word = @"apple";

for (NSString *name in availableDictionariesKeyedByName) {
id dictionary = [availableDictionariesKeyedByName objectForKey:name];
CFRange termRange = DCSGetTermRangeInString((__bridge DCSDictionaryRef)dictionary,
(__bridge CFStringRef)word, 0);
if (termRange.location == kCFNotFound) {
continue;
}

NSString *term = [word substringWithRange:NSMakeRange(termRange.location, termRange.length)];

NSArray *records =
(__bridge_transfer NSArray *)DCSCopyRecordsForSearchString(
(__bridge DCSDictionaryRef)dictionary,
(__bridge CFStringRef)term, NULL, NULL);
if (records) {
for (id record in records) {
NSString *headword = (__bridge NSString *)DCSRecordGetHeadword((__bridge CFTypeRef)record);
if (headword) {
NSString *definition =
(__bridge_transfer NSString *)DCSCopyTextDefinition(
(__bridge DCSDictionaryRef)dictionary,
(__bridge CFStringRef)headword,
CFRangeMake(0, [headword length]));
NSLog(@"%@: %@", name, definition);

NSString *HTML =
(__bridge_transfer NSString *)DCSRecordCopyData(
(__bridge DSCDictionaryRef)dictionary,
(__bridge CFStringRef)headword,
CFRangeMake(0, [headword length]));
NSLog(@"%@: %@", name, definition);
}
}
}
}

从这个实验最让人惊讶的是能够访问原始 HTML 的条目,结合字典捆绑的 CSS,产生 Dictionary.app 中显示的内容。

0x04 iOS

iOS 开发显然是一个更按部就班的事情,所以试图对平台进行逆向工程将只是一个学术练习。幸运的是,通过晦涩的 UIKit 类 UIReferenceLibraryViewController 有大量的功能可用(从 iOS 5 开始)。

UIReferenceLibraryViewControllerMFMessageComposeViewController 很像,其提供了一个围绕系统功能的最小可配置视图控制器,旨在以模态方式呈现。

只需用所需的 term 初始化并模态化显示:

1
2
3
UIReferenceLibraryViewController *referenceLibraryViewController =
[[UIReferenceLibraryViewController alloc] initWithTerm:@"apple"];
[viewController presentViewController:referenceLibraryViewController animated:YES completion:nil];

这跟在 UITextView 中在高亮文字上点击 “Define” UIMenuItem 的行为很像。

UIReferenceLibraryViewController 也提供了类方法 dictionaryHasDefinitionForTerm:。开发者最好在模态一个字典视图控制器前调用这个方法,不然可能啥都没有显示出来。

1
[UIReferenceLibraryViewController dictionaryHasDefinitionForTerm:@"apple"];

从 Unix 的词列表到在 OS X 上(也可能是 iOS)演化的 .dictionary 包,文字是对应用程序编程以及数学常量和 “Sosumi” 警告声至关重要的。思考如何将上述 API 集成到自己的 app 中,或创建一种你以前没有想过的 app。Apple 的平台中有大量的语言学技术,所以要利用它们。

虽然我们都来自不同的背景,有不同的观点,塑造我们的经历;虽然我们做带有各自动机,信念,偏见和意见的事情,使我们彼此分离,但是有一件事情我们是一样的:

我们都必须使用 Xcode。不管怎样,好的或坏的。

至于常见的原因,老实说,事情可以更绝望。也有几个明显的例外,Xcode 好像随着每个版本在变得更好。

但是当然,Xcode 并不真的只是一个应用程序。在 GUI 下是一个应用程序和命令行工具的联合。

0x05 Xcode Tools

0x0501 xcode-select

任何人与 Xcode 的旅程始于一个选择。xcode-select 提供这个选择。