├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── README_CN.md
├── ccl_bplist.py
├── icon.png
├── info.plist
├── mac_alias
├── __init__.py
├── alias.py
├── bookmark.py
├── osx.py
└── utils.py
├── main.py
└── pinyin
├── Mandarin.dat
├── __init__.py
├── _compat.py
├── cmd.py
└── pinyin.py
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: mpco
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | *.pyc
107 | *.pyo
108 |
109 | # macOS
110 | # General
111 | .DS_Store
112 | .AppleDouble
113 | .LSOverride
114 |
115 | # Thumbnails
116 | ._*
117 |
118 | # Files that might appear in the root of a volume
119 | .DocumentRevisions-V100
120 | .fseventsd
121 | .Spotlight-V100
122 | .TemporaryItems
123 | .Trashes
124 | .VolumeIcon.icns
125 | .com.apple.timemachine.donotpresent
126 |
127 | # Directories potentially created on remote AFP share
128 | .AppleDB
129 | .AppleDesktop
130 | .apdisk
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Charles Ma
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: build open
2 |
3 | open:
4 | open ~/Desktop/RecentDocuments.alfredworkflow
5 |
6 | build:
7 | zip -r ~/Desktop/RecentDocuments.alfredworkflow . -x '*.git*'
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Recent Documents / Apps
2 |
3 | Quickly access recent documents and apps.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 中文说明 •
16 | Download •
17 | How To Use •
18 | Configuration
19 |
20 |
21 | **Sorry, no more updates as I no longer use a Mac computer and the installation of macOS virtual machine failed.**
22 |
23 | ## How to Use
24 |
25 | You can press `Enter` to open the file in result, or press `⌘CMD-Enter` to reveal it in Finder.
26 |
27 | ### Tap `rr` to list files opened recently by the foremost app.
28 |
29 | For example:
30 |
31 | - Recent folders will be listed when Finder is foremost.
32 | - Recent rtf, text files will be listed when TextEdit app is foremost.
33 | - Recent *.sketch files will be listed when Sketch app is foremost.
34 | - Recent *.xcodeproj project files will be listed when Xcode app is foremost.
35 |
36 | The subtitle of each result consists of **⏱modified time** and **📡path** of the file.
37 |
38 | 
39 |
40 | ### Tap `rf` to list recent folders.
41 |
42 | Opening recent folders is very common in use. Tapping `rf` is a more efficient way, even though you can activate Finder and then tap `rr`.
43 |
44 | 
45 |
46 | ### Tap `rd` to list recent files.
47 |
48 | These files were recently opened by user, not like `rr` which is just for the foremost app.
49 |
50 | 
51 |
52 | ### Tap `ra` to list apps opened recently.
53 |
54 | 
55 |
56 | ## Configuration
57 |
58 | **Exclude files and folders from the results.**
59 |
60 | You can add private folder paths separated by colon `:` to the workflow environment variable `ExcludedFolders`. The results will not show private folders and any files inside them. The environment variable `ExcludedFiles` is used to block files and folders themselves.
61 |
62 | For example: `~/privateFolder1/:/Users/G/privateFolder2/`
63 |
64 | ### Optional Setup
65 |
66 | 1. Adjust keywords `rr、rf、rd、ra` as you like.
67 | 2. Open `System Preferences - General`, change the number of `Recent items` to 15 or more.
68 |
69 | ## Donation
70 |
71 | Feel free to donate by Wechat if this workflow is helpful.
72 |
73 | 
74 |
75 | ## Dependencies:
76 |
77 | * macMRU-Parser: https://github.com/mac4n6/macMRU-Parser
78 | * ccl_bplist.py: https://github.com/cclgroupltd/ccl-bplist
79 | * mac\_alias: https://pypi.python.org/pypi/mac_alias
80 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # Recent Documents / Apps
2 |
3 | 快速打开最近访问的文档、文件夹、应用。
4 | 快速打开当前应用的最近访问文件。
5 |
6 | 系统要求:macOS 10.11+。
7 | 10.11 未测试
8 |
9 | [下载 Workflow](https://github.com/mpco/Alfred3-workflow-recent-documents/releases)
10 |
11 | ## 用法说明
12 |
13 | - 使用`上下方向键`或`Ctrl-Up/Down`或`文件名过滤`选择列表中的结果。
14 | - 对于中文文件名,支持拼音过滤。每个字的拼音之间需加空格。
15 | 如:`爱国教育.mp4`对应过滤语句`ai guo jiao yu .mp4`。[详细说明](https://github.com/mpco/Alfred3-workflow-recent-documents/releases/tag/2.5)
16 | - 按下 回车键 打开搜索结果中的文件。
17 | - 按下 ⌘CMD+回车键 以在访达中显示文件。
18 |
19 | ### 输入`rr`,列出当前激活应用的最近文档。
20 |
21 | 举个栗子🌰️ :
22 |
23 | - 访达(Finder)在最前,则列出最近访问的文件夹。这样就不用再一层层地找**最近访问过**或**刚刚关闭**的文件夹了。
24 | - 文本编辑(TextEdit)在最前,则列出最近打开过的 rtf、txt 文件。
25 | - Sketch 在最前,则列出最近打开过的 Sketch 设计文档。
26 | - Xcode 在最前,则列出最近打开过的 Xcode 工程。
27 |
28 | 每个条目的子标题由该文件的**⏱修改日期**与**📡路径**组成。
29 |
30 | 这个功能本质上是列出应用菜单栏中` 文件 - 打开最近使用 `内的最近文档列表。所以只要应用的菜单栏中存在` 文件 - 打开最近使用 `菜单项,就能正常工作。点击` 文件 - 打开最近使用 `中的`清除菜单` 则可清理列表。清理后,输入`rr`会显示`没有最近文件记录(None Recent Record)`。
31 |
32 | 
33 |
34 | ### 输入`rf`,列出最近访问的文件夹。
35 |
36 | 工作过程中,常常需要打开最近访问的文件夹。虽然可以切换至访达而后输入`rr`,但为了更加高效,故额外增加了该功能。
37 |
38 | 
39 |
40 | ### 输入`rd`,列出最近打开的各种文件。
41 | 该功能与第一个`rf`的区别在于,`rd`会列出全局的最近文档,而非针对当前应用。
42 |
43 | 
44 |
45 | ### 输入`ra`,列出最近打开的应用。
46 |
47 | 
48 |
49 | ### 排除隐私文件夹
50 |
51 | 你可能希望某些文件或文件夹不要出现在结果中,比如 *.avi 之类的。可以在 Workflow 环境变量`ExcludedFolders`中加入以冒号分隔的文件夹路径。这些文件夹以及其中的任何文件都不会出现在结果中。
52 |
53 | 举例:`~/privateFolder1/:/Users/G/privateFolder2/`
54 |
55 | 
56 |
57 | ### 可选配置:
58 |
59 | 1. 调整`rr、rf、rd、ra`这些关键词,以更加符合你的习惯或需要。
60 | 2. 打开`系统偏好设置 - 通用`,将`最近使用的项目`调整至 15 个或更多。这是因为默认的数量是 10 个,而最近使用的项目记录中有时存在已删除的文件,该 Workflow 会滤除这些已删除的结果,可能导致显示的结果偏少。
61 |
62 | ## 互助互爱
63 |
64 | 哈哈哈,这个 Workflow 是不是很棒,简直想给自己一个么么哒~
65 | 如果这个 Workflow 让你感到很好用,请慷慨赞助(微信扫码)。
66 |
67 | 
68 |
69 |
70 |
71 | ## 依赖项目
72 |
73 | * macMRU-Parser: https://github.com/mac4n6/macMRU-Parser
74 | * ccl_bplist.py: https://github.com/cclgroupltd/ccl-bplist
75 | * mac\_alias: https://pypi.python.org/pypi/mac_alias
76 |
--------------------------------------------------------------------------------
/ccl_bplist.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2012-2016, CCL Forensics
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of the CCL Forensics nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL CCL FORENSICS BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | """
27 |
28 | import sys
29 | import os
30 | import struct
31 | import datetime
32 |
33 | __version__ = "0.21"
34 | __description__ = "Converts Apple binary PList files into a native Python data structure"
35 | __contact__ = "Alex Caithness"
36 |
37 | _object_converter = None
38 | def set_object_converter(function):
39 | """Sets the object converter function to be used when retrieving objects from the bplist.
40 | default is None (which will return objects in their raw form).
41 | A built in converter (ccl_bplist.NSKeyedArchiver_common_objects_convertor) which is geared
42 | toward dealling with common types in NSKeyedArchiver is available which can simplify code greatly
43 | when dealling with these types of files."""
44 | if not hasattr(function, "__call__"):
45 | raise TypeError("function is not a function")
46 | global _object_converter
47 | _object_converter = function
48 |
49 | class BplistError(Exception):
50 | pass
51 |
52 | class BplistUID:
53 | def __init__(self, value):
54 | self.value = value
55 |
56 | def __repr__(self):
57 | return "UID: {0}".format(self.value)
58 |
59 | def __str__(self):
60 | return self.__repr__()
61 |
62 | def __decode_multibyte_int(b, signed=True):
63 | if len(b) == 1:
64 | fmt = ">B" # Always unsigned?
65 | elif len(b) == 2:
66 | fmt = ">h"
67 | elif len(b) == 3:
68 | if signed:
69 | return ((b[0] << 16) | struct.unpack(">H", b[1:])[0]) - ((b[0] >> 7) * 2 * 0x800000)
70 | else:
71 | return (b[0] << 16) | struct.unpack(">H", b[1:])[0]
72 | elif len(b) == 4:
73 | fmt = ">i"
74 | elif len(b) == 8:
75 | fmt = ">q"
76 | elif len(b) == 16:
77 | # special case for BigIntegers
78 | high, low = struct.unpack(">QQ", b)
79 | result = (high << 64) | low
80 | if high & 0x8000000000000000 and signed:
81 | result -= 0x100000000000000000000000000000000
82 | return result
83 | else:
84 | raise BplistError("Cannot decode multibyte int of length {0}".format(len(b)))
85 |
86 | if signed and len(b) > 1:
87 | return struct.unpack(fmt.lower(), b)[0]
88 | else:
89 | return struct.unpack(fmt.upper(), b)[0]
90 |
91 | def __decode_float(b, signed=True):
92 | if len(b) == 4:
93 | fmt = ">f"
94 | elif len(b) == 8:
95 | fmt = ">d"
96 | else:
97 | raise BplistError("Cannot decode float of length {0}".format(len(b)))
98 |
99 | if signed:
100 | return struct.unpack(fmt.lower(), b)[0]
101 | else:
102 | return struct.unpack(fmt.upper(), b)[0]
103 |
104 | def __decode_object(f, offset, collection_offset_size, offset_table):
105 | # Move to offset and read type
106 | #print("Decoding object at offset {0}".format(offset))
107 | f.seek(offset)
108 | # A little hack to keep the script portable between py2.x and py3k
109 | if sys.version_info[0] < 3:
110 | type_byte = ord(f.read(1)[0])
111 | else:
112 | type_byte = f.read(1)[0]
113 | #print("Type byte: {0}".format(hex(type_byte)))
114 | if type_byte == 0x00: # Null 0000 0000
115 | return None
116 | elif type_byte == 0x08: # False 0000 1000
117 | return False
118 | elif type_byte == 0x09: # True 0000 1001
119 | return True
120 | elif type_byte == 0x0F: # Fill 0000 1111
121 | raise BplistError("Fill type not currently supported at offset {0}".format(f.tell())) # Not sure what to return really...
122 | elif type_byte & 0xF0 == 0x10: # Int 0001 xxxx
123 | int_length = 2 ** (type_byte & 0x0F)
124 | int_bytes = f.read(int_length)
125 | return __decode_multibyte_int(int_bytes)
126 | elif type_byte & 0xF0 == 0x20: # Float 0010 nnnn
127 | float_length = 2 ** (type_byte & 0x0F)
128 | float_bytes = f.read(float_length)
129 | return __decode_float(float_bytes)
130 | elif type_byte & 0xFF == 0x33: # Date 0011 0011
131 | date_bytes = f.read(8)
132 | date_value = __decode_float(date_bytes)
133 | try:
134 | result = datetime.datetime(2001,1,1) + datetime.timedelta(seconds = date_value)
135 | except OverflowError:
136 | result = datetime.datetime.min
137 | return result
138 | elif type_byte & 0xF0 == 0x40: # Data 0100 nnnn
139 | if type_byte & 0x0F != 0x0F:
140 | # length in 4 lsb
141 | data_length = type_byte & 0x0F
142 | else:
143 | # A little hack to keep the script portable between py2.x and py3k
144 | if sys.version_info[0] < 3:
145 | int_type_byte = ord(f.read(1)[0])
146 | else:
147 | int_type_byte = f.read(1)[0]
148 | if int_type_byte & 0xF0 != 0x10:
149 | raise BplistError("Long Data field definition not followed by int type at offset {0}".format(f.tell()))
150 | int_length = 2 ** (int_type_byte & 0x0F)
151 | int_bytes = f.read(int_length)
152 | data_length = __decode_multibyte_int(int_bytes, False)
153 | return f.read(data_length)
154 | elif type_byte & 0xF0 == 0x50: # ASCII 0101 nnnn
155 | if type_byte & 0x0F != 0x0F:
156 | # length in 4 lsb
157 | ascii_length = type_byte & 0x0F
158 | else:
159 | # A little hack to keep the script portable between py2.x and py3k
160 | if sys.version_info[0] < 3:
161 | int_type_byte = ord(f.read(1)[0])
162 | else:
163 | int_type_byte = f.read(1)[0]
164 | if int_type_byte & 0xF0 != 0x10:
165 | raise BplistError("Long ASCII field definition not followed by int type at offset {0}".format(f.tell()))
166 | int_length = 2 ** (int_type_byte & 0x0F)
167 | int_bytes = f.read(int_length)
168 | ascii_length = __decode_multibyte_int(int_bytes, False)
169 | return f.read(ascii_length).decode("ascii")
170 | elif type_byte & 0xF0 == 0x60: # UTF-16 0110 nnnn
171 | if type_byte & 0x0F != 0x0F:
172 | # length in 4 lsb
173 | utf16_length = (type_byte & 0x0F) * 2 # Length is characters - 16bit width
174 | else:
175 | # A little hack to keep the script portable between py2.x and py3k
176 | if sys.version_info[0] < 3:
177 | int_type_byte = ord(f.read(1)[0])
178 | else:
179 | int_type_byte = f.read(1)[0]
180 | if int_type_byte & 0xF0 != 0x10:
181 | raise BplistError("Long UTF-16 field definition not followed by int type at offset {0}".format(f.tell()))
182 | int_length = 2 ** (int_type_byte & 0x0F)
183 | int_bytes = f.read(int_length)
184 | utf16_length = __decode_multibyte_int(int_bytes, False) * 2
185 | return f.read(utf16_length).decode("utf_16_be")
186 | elif type_byte & 0xF0 == 0x80: # UID 1000 nnnn
187 | uid_length = (type_byte & 0x0F) + 1
188 | uid_bytes = f.read(uid_length)
189 | return BplistUID(__decode_multibyte_int(uid_bytes, signed=False))
190 | elif type_byte & 0xF0 == 0xA0: # Array 1010 nnnn
191 | if type_byte & 0x0F != 0x0F:
192 | # length in 4 lsb
193 | array_count = type_byte & 0x0F
194 | else:
195 | # A little hack to keep the script portable between py2.x and py3k
196 | if sys.version_info[0] < 3:
197 | int_type_byte = ord(f.read(1)[0])
198 | else:
199 | int_type_byte = f.read(1)[0]
200 | if int_type_byte & 0xF0 != 0x10:
201 | raise BplistError("Long Array field definition not followed by int type at offset {0}".format(f.tell()))
202 | int_length = 2 ** (int_type_byte & 0x0F)
203 | int_bytes = f.read(int_length)
204 | array_count = __decode_multibyte_int(int_bytes, signed=False)
205 | array_refs = []
206 | for i in range(array_count):
207 | array_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
208 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in array_refs]
209 | elif type_byte & 0xF0 == 0xC0: # Set 1010 nnnn
210 | if type_byte & 0x0F != 0x0F:
211 | # length in 4 lsb
212 | set_count = type_byte & 0x0F
213 | else:
214 | # A little hack to keep the script portable between py2.x and py3k
215 | if sys.version_info[0] < 3:
216 | int_type_byte = ord(f.read(1)[0])
217 | else:
218 | int_type_byte = f.read(1)[0]
219 | if int_type_byte & 0xF0 != 0x10:
220 | raise BplistError("Long Set field definition not followed by int type at offset {0}".format(f.tell()))
221 | int_length = 2 ** (int_type_byte & 0x0F)
222 | int_bytes = f.read(int_length)
223 | set_count = __decode_multibyte_int(int_bytes, signed=False)
224 | set_refs = []
225 | for i in range(set_count):
226 | set_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
227 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in set_refs]
228 | elif type_byte & 0xF0 == 0xD0: # Dict 1011 nnnn
229 | if type_byte & 0x0F != 0x0F:
230 | # length in 4 lsb
231 | dict_count = type_byte & 0x0F
232 | else:
233 | # A little hack to keep the script portable between py2.x and py3k
234 | if sys.version_info[0] < 3:
235 | int_type_byte = ord(f.read(1)[0])
236 | else:
237 | int_type_byte = f.read(1)[0]
238 | #print("Dictionary length int byte: {0}".format(hex(int_type_byte)))
239 | if int_type_byte & 0xF0 != 0x10:
240 | raise BplistError("Long Dict field definition not followed by int type at offset {0}".format(f.tell()))
241 | int_length = 2 ** (int_type_byte & 0x0F)
242 | int_bytes = f.read(int_length)
243 | dict_count = __decode_multibyte_int(int_bytes, signed=False)
244 | key_refs = []
245 | #print("Dictionary count: {0}".format(dict_count))
246 | for i in range(dict_count):
247 | key_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
248 | value_refs = []
249 | for i in range(dict_count):
250 | value_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
251 |
252 | dict_result = {}
253 | for i in range(dict_count):
254 | #print("Key ref: {0}\tVal ref: {1}".format(key_refs[i], value_refs[i]))
255 | key = __decode_object(f, offset_table[key_refs[i]], collection_offset_size, offset_table)
256 | val = __decode_object(f, offset_table[value_refs[i]], collection_offset_size, offset_table)
257 | dict_result[key] = val
258 | return dict_result
259 |
260 |
261 | def load(f):
262 | """
263 | Reads and converts a file-like object containing a binary property list.
264 | Takes a file-like object (must support reading and seeking) as an argument
265 | Returns a data structure representing the data in the property list
266 | """
267 | # Check magic number
268 | if f.read(8) != b"bplist00":
269 | raise BplistError("Bad file header")
270 |
271 | # Read trailer
272 | f.seek(-32, os.SEEK_END)
273 | trailer = f.read(32)
274 | offset_int_size, collection_offset_size, object_count, top_level_object_index, offest_table_offset = struct.unpack(">6xbbQQQ", trailer)
275 |
276 | # Read offset table
277 | f.seek(offest_table_offset)
278 | offset_table = []
279 | for i in range(object_count):
280 | offset_table.append(__decode_multibyte_int(f.read(offset_int_size), False))
281 |
282 | return __decode_object(f, offset_table[top_level_object_index], collection_offset_size, offset_table)
283 |
284 |
285 | def NSKeyedArchiver_common_objects_convertor(o):
286 | """Built in converter function (suitable for submission to set_object_converter()) which automatically
287 | converts the following common data-types found in NSKeyedArchiver:
288 | NSDictionary/NSMutableDictionary;
289 | NSArray/NSMutableArray;
290 | NSSet/NSMutableSet
291 | NSString/NSMutableString
292 | NSDate
293 | $null strings"""
294 | # Conversion: NSDictionary
295 | if is_nsmutabledictionary(o):
296 | return convert_NSMutableDictionary(o)
297 | # Conversion: NSArray
298 | elif is_nsarray(o):
299 | return convert_NSArray(o)
300 | elif is_isnsset(o):
301 | return convert_NSSet(o)
302 | # Conversion: NSString
303 | elif is_nsstring(o):
304 | return convert_NSString(o)
305 | # Conversion: NSDate
306 | elif is_nsdate(o):
307 | return convert_NSDate(o)
308 | # Conversion: "$null" string
309 | elif isinstance(o, str) and o == "$null":
310 | return None
311 | # Fallback:
312 | else:
313 | return o
314 |
315 | def NSKeyedArchiver_convert(o, object_table):
316 | if isinstance(o, list):
317 | #return NsKeyedArchiverList(o, object_table)
318 | result = NsKeyedArchiverList(o, object_table)
319 | elif isinstance(o, dict):
320 | #return NsKeyedArchiverDictionary(o, object_table)
321 | result = NsKeyedArchiverDictionary(o, object_table)
322 | elif isinstance(o, BplistUID):
323 | #return NSKeyedArchiver_convert(object_table[o.value], object_table)
324 | result = NSKeyedArchiver_convert(object_table[o.value], object_table)
325 | else:
326 | #return o
327 | result = o
328 |
329 | if _object_converter:
330 | return _object_converter(result)
331 | else:
332 | return result
333 |
334 |
335 | class NsKeyedArchiverDictionary(dict):
336 | def __init__(self, original_dict, object_table):
337 | super(NsKeyedArchiverDictionary, self).__init__(original_dict)
338 | self.object_table = object_table
339 |
340 | def __getitem__(self, index):
341 | o = super(NsKeyedArchiverDictionary, self).__getitem__(index)
342 | return NSKeyedArchiver_convert(o, self.object_table)
343 |
344 | def get(self, key, default=None):
345 | return self[key] if key in self else default
346 |
347 | def values(self):
348 | for k in self:
349 | yield self[k]
350 |
351 | def items(self):
352 | for k in self:
353 | yield k, self[k]
354 |
355 | class NsKeyedArchiverList(list):
356 | def __init__(self, original_iterable, object_table):
357 | super(NsKeyedArchiverList, self).__init__(original_iterable)
358 | self.object_table = object_table
359 |
360 | def __getitem__(self, index):
361 | o = super(NsKeyedArchiverList, self).__getitem__(index)
362 | return NSKeyedArchiver_convert(o, self.object_table)
363 |
364 | def __iter__(self):
365 | for o in super(NsKeyedArchiverList, self).__iter__():
366 | yield NSKeyedArchiver_convert(o, self.object_table)
367 |
368 |
369 | def deserialise_NsKeyedArchiver(obj, parse_whole_structure=False):
370 | """Deserialises an NSKeyedArchiver bplist rebuilding the structure.
371 | obj should usually be the top-level object returned by the load()
372 | function."""
373 |
374 | # Check that this is an archiver and version we understand
375 | if not isinstance(obj, dict):
376 | raise TypeError("obj must be a dict")
377 | if "$archiver" not in obj or obj["$archiver"] not in ("NSKeyedArchiver", "NRKeyedArchiver"):
378 | raise ValueError("obj does not contain an '$archiver' key or the '$archiver' is unrecognised")
379 | if "$version" not in obj or obj["$version"] != 100000:
380 | raise ValueError("obj does not contain a '$version' key or the '$version' is unrecognised")
381 |
382 | object_table = obj["$objects"]
383 | if "root" in obj["$top"] and not parse_whole_structure:
384 | return NSKeyedArchiver_convert(obj["$top"]["root"], object_table)
385 | else:
386 | return NSKeyedArchiver_convert(obj["$top"], object_table)
387 |
388 | # NSMutableDictionary convenience functions
389 | def is_nsmutabledictionary(obj):
390 | if not isinstance(obj, dict):
391 | return False
392 | if "$class" not in obj.keys():
393 | return False
394 | if obj["$class"].get("$classname") not in ("NSMutableDictionary", "NSDictionary"):
395 | return False
396 | if "NS.keys" not in obj.keys():
397 | return False
398 | if "NS.objects" not in obj.keys():
399 | return False
400 |
401 | return True
402 |
403 | def convert_NSMutableDictionary(obj):
404 | """Converts a NSKeyedArchiver serialised NSMutableDictionary into
405 | a straight dictionary (rather than two lists as it is serialised
406 | as)"""
407 |
408 | # The dictionary is serialised as two lists (one for keys and one
409 | # for values) which obviously removes all convenience afforded by
410 | # dictionaries. This function converts this structure to an
411 | # actual dictionary so that values can be accessed by key.
412 |
413 | if not is_nsmutabledictionary(obj):
414 | raise ValueError("obj does not have the correct structure for a NSDictionary/NSMutableDictionary serialised to a NSKeyedArchiver")
415 | keys = obj["NS.keys"]
416 | vals = obj["NS.objects"]
417 |
418 | # sense check the keys and values:
419 | if not isinstance(keys, list):
420 | raise TypeError("The 'NS.keys' value is an unexpected type (expected list; actual: {0}".format(type(keys)))
421 | if not isinstance(vals, list):
422 | raise TypeError("The 'NS.objects' value is an unexpected type (expected list; actual: {0}".format(type(vals)))
423 | if len(keys) != len(vals):
424 | raise ValueError("The length of the 'NS.keys' list ({0}) is not equal to that of the 'NS.objects ({1})".format(len(keys), len(vals)))
425 |
426 | result = {}
427 | for i,k in enumerate(keys):
428 | if k in result:
429 | raise ValueError("The 'NS.keys' list contains duplicate entries")
430 | result[k] = vals[i]
431 |
432 | return result
433 |
434 | # NSArray convenience functions
435 | def is_nsarray(obj):
436 | if not isinstance(obj, dict):
437 | return False
438 | if "$class" not in obj.keys():
439 | return False
440 | if obj["$class"].get("$classname") not in ("NSArray", "NSMutableArray"):
441 | return False
442 | if "NS.objects" not in obj.keys():
443 | return False
444 |
445 | return True
446 |
447 | def convert_NSArray(obj):
448 | if not is_nsarray(obj):
449 | raise ValueError("obj does not have the correct structure for a NSArray/NSMutableArray serialised to a NSKeyedArchiver")
450 |
451 | return obj["NS.objects"]
452 |
453 | # NSSet convenience functions
454 | def is_isnsset(obj):
455 | if not isinstance(obj, dict):
456 | return False
457 | if "$class" not in obj.keys():
458 | return False
459 | if obj["$class"].get("$classname") not in ("NSSet", "NSMutableSet"):
460 | return False
461 | if "NS.objects" not in obj.keys():
462 | return False
463 |
464 | return True
465 |
466 | def convert_NSSet(obj):
467 | if not is_isnsset(obj):
468 | raise ValueError("obj does not have the correct structure for a NSSet/NSMutableSet serialised to a NSKeyedArchiver")
469 |
470 | return list(obj["NS.objects"])
471 |
472 | # NSString convenience functions
473 | def is_nsstring(obj):
474 | if not isinstance(obj, dict):
475 | return False
476 | if "$class" not in obj.keys():
477 | return False
478 | if obj["$class"].get("$classname") not in ("NSString", "NSMutableString"):
479 | return False
480 | if "NS.string" not in obj.keys():
481 | return False
482 | return True
483 |
484 | def convert_NSString(obj):
485 | if not is_nsstring(obj):
486 | raise ValueError("obj does not have the correct structure for a NSString/NSMutableString serialised to a NSKeyedArchiver")
487 |
488 | return obj["NS.string"]
489 |
490 | # NSDate convenience functions
491 | def is_nsdate(obj):
492 | if not isinstance(obj, dict):
493 | return False
494 | if "$class" not in obj.keys():
495 | return False
496 | if obj["$class"].get("$classname") not in ("NSDate"):
497 | return False
498 | if "NS.time" not in obj.keys():
499 | return False
500 |
501 | return True
502 |
503 | def convert_NSDate(obj):
504 | if not is_nsdate(obj):
505 | raise ValueError("obj does not have the correct structure for a NSDate serialised to a NSKeyedArchiver")
506 |
507 | return datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=obj["NS.time"])
508 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mpco/AlfredWorkflow-Recent-Documents/75fad35b380baaf048fedda569eb9b7bf754a5b1/icon.png
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 | com.mpco.recentDocuments
7 | category
8 | Tools
9 | connections
10 |
11 | 242EA50D-B03A-4278-A412-B4AFC16AC51F
12 |
13 |
14 | destinationuid
15 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8
16 | modifiers
17 | 0
18 | modifiersubtext
19 |
20 | vitoclose
21 |
22 |
23 |
24 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96
25 |
26 |
27 | destinationuid
28 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B
29 | modifiers
30 | 0
31 | modifiersubtext
32 |
33 | vitoclose
34 |
35 |
36 |
37 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C
38 |
39 |
40 | destinationuid
41 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96
42 | modifiers
43 | 0
44 | modifiersubtext
45 |
46 | vitoclose
47 |
48 |
49 |
50 | 53BF5810-7308-4550-A1C3-92DC01964658
51 |
52 |
53 | destinationuid
54 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0
55 | modifiers
56 | 0
57 | modifiersubtext
58 |
59 | vitoclose
60 |
61 |
62 |
63 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9
64 |
65 |
66 | destinationuid
67 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316
68 | modifiers
69 | 0
70 | modifiersubtext
71 |
72 | vitoclose
73 |
74 |
75 |
76 | 79D60C12-3480-4744-93FD-5B6AA769F31A
77 |
78 |
79 | destinationuid
80 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E
81 | modifiers
82 | 0
83 | modifiersubtext
84 |
85 | vitoclose
86 |
87 |
88 |
89 | A46BBC3D-A323-49F8-90F1-A58975C5AF06
90 |
91 |
92 | destinationuid
93 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
94 | modifiers
95 | 0
96 | modifiersubtext
97 |
98 | vitoclose
99 |
100 |
101 |
102 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E
103 |
104 |
105 | destinationuid
106 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
107 | modifiers
108 | 0
109 | modifiersubtext
110 |
111 | vitoclose
112 |
113 |
114 |
115 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0
116 |
117 |
118 | destinationuid
119 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
120 | modifiers
121 | 0
122 | modifiersubtext
123 |
124 | vitoclose
125 |
126 |
127 |
128 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316
129 |
130 |
131 | destinationuid
132 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
133 | modifiers
134 | 0
135 | modifiersubtext
136 |
137 | vitoclose
138 |
139 |
140 |
141 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B
142 |
143 |
144 | destinationuid
145 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
146 | modifiers
147 | 0
148 | modifiersubtext
149 |
150 | vitoclose
151 |
152 |
153 |
154 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8
155 |
156 |
157 | destinationuid
158 | A46BBC3D-A323-49F8-90F1-A58975C5AF06
159 | modifiers
160 | 0
161 | modifiersubtext
162 |
163 | vitoclose
164 |
165 |
166 |
167 |
168 | createdby
169 | Charles Ma
170 | description
171 | Open recent documents or apps
172 | disabled
173 |
174 | name
175 | Recent Documents / Apps
176 | objects
177 |
178 |
179 | config
180 |
181 | action
182 | 0
183 | argument
184 | 0
185 | focusedappvariable
186 |
187 | focusedappvariablename
188 |
189 | hotkey
190 | 0
191 | hotmod
192 | 0
193 | leftcursor
194 |
195 | modsmode
196 | 0
197 | relatedAppsMode
198 | 0
199 |
200 | type
201 | alfred.workflow.trigger.hotkey
202 | uid
203 | 79D60C12-3480-4744-93FD-5B6AA769F31A
204 | version
205 | 2
206 |
207 |
208 | config
209 |
210 | alfredfiltersresults
211 |
212 | alfredfiltersresultsmatchmode
213 | 0
214 | argumenttreatemptyqueryasnil
215 |
216 | argumenttrimmode
217 | 0
218 | argumenttype
219 | 1
220 | escaping
221 | 102
222 | keyword
223 | ra
224 | queuedelaycustom
225 | 3
226 | queuedelayimmediatelyinitially
227 |
228 | queuedelaymode
229 | 0
230 | queuemode
231 | 1
232 | runningsubtext
233 |
234 | script
235 | # python -O xxx.py, run
236 | # python xxx.py, debug
237 |
238 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2)
239 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
240 |
241 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then
242 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl"
243 | elif [ $(version $macOSVersion) -lt $(version "12.3") ]; then
244 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl2"
245 | elif [ $(version $macOSVersion) -lt $(version "14.0") ]; then
246 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl2"
247 | else
248 | /usr/bin/python3 -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl3"
249 | fi
250 | scriptargtype
251 | 1
252 | scriptfile
253 |
254 | subtext
255 | Tap "ra" to show
256 | title
257 | Recent apps
258 | type
259 | 0
260 | withspace
261 |
262 |
263 | type
264 | alfred.workflow.input.scriptfilter
265 | uid
266 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E
267 | version
268 | 3
269 |
270 |
271 | config
272 |
273 | openwith
274 |
275 | sourcefile
276 |
277 |
278 | type
279 | alfred.workflow.action.openfile
280 | uid
281 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
282 | version
283 | 3
284 |
285 |
286 | config
287 |
288 | action
289 | 0
290 | argument
291 | 0
292 | focusedappvariable
293 |
294 | focusedappvariablename
295 |
296 | hotkey
297 | 0
298 | hotmod
299 | 0
300 | leftcursor
301 |
302 | modsmode
303 | 0
304 | relatedAppsMode
305 | 0
306 |
307 | type
308 | alfred.workflow.trigger.hotkey
309 | uid
310 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9
311 | version
312 | 2
313 |
314 |
315 | config
316 |
317 | alfredfiltersresults
318 |
319 | alfredfiltersresultsmatchmode
320 | 0
321 | argumenttreatemptyqueryasnil
322 |
323 | argumenttrimmode
324 | 0
325 | argumenttype
326 | 1
327 | escaping
328 | 102
329 | keyword
330 | rd
331 | queuedelaycustom
332 | 3
333 | queuedelayimmediatelyinitially
334 |
335 | queuedelaymode
336 | 0
337 | queuemode
338 | 1
339 | runningsubtext
340 |
341 | script
342 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2)
343 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
344 |
345 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then
346 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl"
347 | elif [ $(version $macOSVersion) -lt $(version "12.3") ]; then
348 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl2"
349 | elif [ $(version $macOSVersion) -lt $(version "14.0") ]; then
350 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl2"
351 | else
352 | /usr/bin/python3 -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl3"
353 | fi
354 | scriptargtype
355 | 1
356 | scriptfile
357 |
358 | subtext
359 | Tap "rd" to show
360 | title
361 | Recent documents
362 | type
363 | 0
364 | withspace
365 |
366 |
367 | type
368 | alfred.workflow.input.scriptfilter
369 | uid
370 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316
371 | version
372 | 3
373 |
374 |
375 | config
376 |
377 | action
378 | 0
379 | argument
380 | 0
381 | focusedappvariable
382 |
383 | focusedappvariablename
384 |
385 | hotkey
386 | 0
387 | hotmod
388 | 0
389 | leftcursor
390 |
391 | modsmode
392 | 0
393 | relatedAppsMode
394 | 0
395 |
396 | type
397 | alfred.workflow.trigger.hotkey
398 | uid
399 | 53BF5810-7308-4550-A1C3-92DC01964658
400 | version
401 | 2
402 |
403 |
404 | config
405 |
406 | alfredfiltersresults
407 |
408 | alfredfiltersresultsmatchmode
409 | 0
410 | argumenttreatemptyqueryasnil
411 |
412 | argumenttrimmode
413 | 0
414 | argumenttype
415 | 1
416 | escaping
417 | 102
418 | keyword
419 | rf
420 | queuedelaycustom
421 | 3
422 | queuedelayimmediatelyinitially
423 |
424 | queuedelaymode
425 | 0
426 | queuemode
427 | 1
428 | runningsubtext
429 |
430 | script
431 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2)
432 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
433 |
434 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then
435 | /usr/bin/python -O main.py "${HOME}/Library/Preferences/com.apple.finder.plist"
436 | else
437 | /usr/bin/python3 -O main.py "${HOME}/Library/Preferences/com.apple.finder.plist"
438 | fi
439 |
440 | scriptargtype
441 | 1
442 | scriptfile
443 |
444 | subtext
445 | Tap "rff" to show
446 | title
447 | Recent folder
448 | type
449 | 0
450 | withspace
451 |
452 |
453 | type
454 | alfred.workflow.input.scriptfilter
455 | uid
456 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0
457 | version
458 | 3
459 |
460 |
461 | config
462 |
463 | json
464 | {
465 | "alfredworkflow" : {
466 | "arg" : "{query}",
467 | "config" : {
468 | "openwith" : "{var:currentAppPath}",
469 | "sourcefile" : ""
470 | },
471 | "variables" : {
472 | }
473 | }
474 | }
475 |
476 | type
477 | alfred.workflow.utility.json
478 | uid
479 | A46BBC3D-A323-49F8-90F1-A58975C5AF06
480 | version
481 | 1
482 |
483 |
484 | config
485 |
486 | action
487 | 0
488 | argument
489 | 0
490 | focusedappvariable
491 |
492 | focusedappvariablename
493 |
494 | hotkey
495 | 0
496 | hotmod
497 | 0
498 | leftcursor
499 |
500 | modsmode
501 | 0
502 | relatedAppsMode
503 | 0
504 |
505 | type
506 | alfred.workflow.trigger.hotkey
507 | uid
508 | 242EA50D-B03A-4278-A412-B4AFC16AC51F
509 | version
510 | 2
511 |
512 |
513 | config
514 |
515 | alfredfiltersresults
516 |
517 | alfredfiltersresultsmatchmode
518 | 0
519 | argumenttreatemptyqueryasnil
520 |
521 | argumenttrimmode
522 | 0
523 | argumenttype
524 | 1
525 | escaping
526 | 102
527 | keyword
528 | rr
529 | queuedelaycustom
530 | 3
531 | queuedelayimmediatelyinitially
532 |
533 | queuedelaymode
534 | 0
535 | queuemode
536 | 1
537 | runningsubtext
538 |
539 | script
540 |
541 | # check macos version
542 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2)
543 |
544 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
545 |
546 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then
547 | MRUfileExt="sfl"
548 | elif [ $(version $macOSVersion) -lt $(version "14.00") ]; then
549 | MRUfileExt="sfl2"
550 | else
551 | MRUfileExt="sfl3"
552 | fi
553 |
554 | # check app
555 | currentAppID=$(osascript -e "tell application (path to frontmost application as text) to get id")
556 |
557 | export currentAppPath=$(osascript -e 'tell application "Finder" to get text 1 thru -2 of POSIX path of (path to frontmost application)')
558 | if [ "${currentAppID}" = "com.apple.finder" ]; then
559 | MRUfile="${HOME}/Library/Preferences/com.apple.finder.plist"
560 | elif [ "${currentAppID}" = "com.microsoft.Word" ] || [ "${currentAppID}" = "com.microsoft.Powerpoint" ] || [ "${currentAppID}" = "com.microsoft.Excel" ] ; then
561 | MRUfile="${HOME}/Library/Containers/${currentAppID}/Data/Library/Preferences/${currentAppID}.securebookmarks.plist"
562 | elif [ "${currentAppID}" = "com.sublimetext.3" ]; then
563 | AutoSaveSessionFile="${HOME}/Library/Application Support/Sublime Text 3/Local/Auto Save Session.sublime_session"
564 | if [ -f "$AutoSaveSessionFile" ]; then
565 | MRUfile="$AutoSaveSessionFile"
566 | else
567 | MRUfile="${HOME}/Library/Application Support/Sublime Text 3/Local/Session.sublime_session"
568 | fi
569 | else
570 | MRUfile="${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/${currentAppID}.${MRUfileExt}"
571 | fi
572 |
573 |
574 | if [ -f "${MRUfile}" ]; then
575 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then
576 | /usr/bin/python -O main.py "${MRUfile}"
577 | else
578 | /usr/bin/python3 -O main.py "${MRUfile}"
579 | fi
580 | else
581 | echo '{"items": [{"title": "None Recent Record for the foremost app","subtitle": "(*´・д・)?"}]}'
582 | fi
583 | scriptargtype
584 | 1
585 | scriptfile
586 |
587 | subtext
588 | Tap "rf" to show
589 | title
590 | Recent documents for the foremost app
591 | type
592 | 0
593 | withspace
594 |
595 |
596 | type
597 | alfred.workflow.input.scriptfilter
598 | uid
599 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8
600 | version
601 | 3
602 |
603 |
604 | config
605 |
606 | json
607 | {
608 | "alfredworkflow" : {
609 | "arg" : "{query}",
610 | "config" : {
611 | "openwith" : "{var:currentAppPath}",
612 | "sourcefile" : ""
613 | },
614 | "variables" : {
615 | }
616 | }
617 | }
618 |
619 | type
620 | alfred.workflow.utility.json
621 | uid
622 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B
623 | version
624 | 1
625 |
626 |
627 | config
628 |
629 | alfredfiltersresults
630 |
631 | alfredfiltersresultsmatchmode
632 | 0
633 | argumenttreatemptyqueryasnil
634 |
635 | argumenttrimmode
636 | 0
637 | argumenttype
638 | 1
639 | escaping
640 | 102
641 | keyword
642 | code
643 | queuedelaycustom
644 | 3
645 | queuedelayimmediatelyinitially
646 |
647 | queuedelaymode
648 | 0
649 | queuemode
650 | 1
651 | runningsubtext
652 |
653 | script
654 |
655 | # check macos version
656 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2)
657 |
658 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
659 |
660 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then
661 | MRUfileExt="sfl"
662 | elif [ $(version $macOSVersion) -lt $(version "14.00") ]; then
663 | MRUfileExt="sfl2"
664 | else
665 | MRUfileExt="sfl3"
666 | fi
667 |
668 | # check app
669 | # currentAppID=$(osascript -e "tell application (path to frontmost application as text) to get id")
670 | currentAppID="com.microsoft.VSCode"
671 | # export currentAppPath=$(osascript -e 'tell application "Finder" to get text 1 thru -2 of POSIX path of (path to frontmost application)')
672 | export currentAppPath="/Applications/Visual Studio Code.app"
673 | if [ "${currentAppID}" = "com.apple.finder" ]; then
674 | MRUfile="${HOME}/Library/Preferences/com.apple.finder.plist"
675 | elif [ "${currentAppID}" = "com.microsoft.Word" ] || [ "${currentAppID}" = "com.microsoft.Powerpoint" ] || [ "${currentAppID}" = "com.microsoft.Excel" ] ; then
676 | MRUfile="${HOME}/Library/Containers/${currentAppID}/Data/Library/Preferences/${currentAppID}.securebookmarks.plist"
677 | elif [ "${currentAppID}" = "com.sublimetext.3" ]; then
678 | AutoSaveSessionFile="${HOME}/Library/Application Support/Sublime Text 3/Local/Auto Save Session.sublime_session"
679 | if [ -f "$AutoSaveSessionFile" ]; then
680 | MRUfile="$AutoSaveSessionFile"
681 | else
682 | MRUfile="${HOME}/Library/Application Support/Sublime Text 3/Local/Session.sublime_session"
683 | fi
684 | else
685 | MRUfile="${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/${currentAppID}.${MRUfileExt}"
686 | fi
687 |
688 |
689 | if [ -f "${MRUfile}" ]; then
690 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then
691 | /usr/bin/python -O main.py "${MRUfile}"
692 | else
693 | /usr/bin/python3 -O main.py "${MRUfile}"
694 | fi
695 | else
696 | echo '{"items": [{"title": "None Recent Record for the foremost app","subtitle": "(*´・д・)?"}]}'
697 | fi
698 | scriptargtype
699 | 1
700 | scriptfile
701 |
702 | subtext
703 | Tap "code" to show
704 | title
705 | Recent documents for the vscode
706 | type
707 | 0
708 | withspace
709 |
710 |
711 | type
712 | alfred.workflow.input.scriptfilter
713 | uid
714 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96
715 | version
716 | 3
717 |
718 |
719 | config
720 |
721 | action
722 | 0
723 | argument
724 | 0
725 | focusedappvariable
726 |
727 | focusedappvariablename
728 |
729 | hotkey
730 | 0
731 | hotmod
732 | 0
733 | leftcursor
734 |
735 | modsmode
736 | 0
737 | relatedAppsMode
738 | 0
739 |
740 | type
741 | alfred.workflow.trigger.hotkey
742 | uid
743 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C
744 | version
745 | 2
746 |
747 |
748 | readme
749 | Website: https://github.com/mpco/AlfredWorkflow-Recent-Documents
750 | uidata
751 |
752 | 242EA50D-B03A-4278-A412-B4AFC16AC51F
753 |
754 | xpos
755 | 90
756 | ypos
757 | 515
758 |
759 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96
760 |
761 | note
762 | Foremost App's Recent Documents
763 | xpos
764 | 255
765 | ypos
766 | 690
767 |
768 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C
769 |
770 | xpos
771 | 80
772 | ypos
773 | 690
774 |
775 | 53BF5810-7308-4550-A1C3-92DC01964658
776 |
777 | xpos
778 | 100
779 | ypos
780 | 365
781 |
782 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9
783 |
784 | xpos
785 | 100
786 | ypos
787 | 205
788 |
789 | 79D60C12-3480-4744-93FD-5B6AA769F31A
790 |
791 | xpos
792 | 100
793 | ypos
794 | 50
795 |
796 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595
797 |
798 | xpos
799 | 700
800 | ypos
801 | 50
802 |
803 | A46BBC3D-A323-49F8-90F1-A58975C5AF06
804 |
805 | xpos
806 | 475
807 | ypos
808 | 420
809 |
810 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E
811 |
812 | note
813 | Recent Apps
814 | xpos
815 | 270
816 | ypos
817 | 50
818 |
819 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0
820 |
821 | note
822 | Recent Folders
823 | xpos
824 | 270
825 | ypos
826 | 365
827 |
828 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316
829 |
830 | note
831 | Recent Documents
832 | xpos
833 | 270
834 | ypos
835 | 205
836 |
837 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B
838 |
839 | xpos
840 | 465
841 | ypos
842 | 595
843 |
844 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8
845 |
846 | note
847 | Foremost App's Recent Documents
848 | xpos
849 | 265
850 | ypos
851 | 515
852 |
853 |
854 | userconfigurationconfig
855 |
856 | variables
857 |
858 | ExcludedFiles
859 |
860 | ExcludedFolders
861 |
862 |
863 | variablesdontexport
864 |
865 | ExcludedFiles
866 | ExcludedFolders
867 |
868 | version
869 | 2.20
870 | webaddress
871 | https://github.com/mpco/AlfredWorkflow-Recent-Documents
872 |
873 |
874 |
--------------------------------------------------------------------------------
/mac_alias/__init__.py:
--------------------------------------------------------------------------------
1 | from .alias import *
2 | from .bookmark import *
3 |
4 | __all__ = [ 'ALIAS_KIND_FILE', 'ALIAS_KIND_FOLDER',
5 | 'ALIAS_HFS_VOLUME_SIGNATURE',
6 | 'ALIAS_FIXED_DISK', 'ALIAS_NETWORK_DISK', 'ALIAS_400KB_FLOPPY_DISK',
7 | 'ALIAS_800KB_FLOPPY_DISK', 'ALIAS_1_44MB_FLOPPY_DISK',
8 | 'ALIAS_EJECTABLE_DISK',
9 | 'ALIAS_NO_CNID',
10 | 'kBookmarkPath', 'kBookmarkCNIDPath', 'kBookmarkFileProperties',
11 | 'kBookmarkFileName', 'kBookmarkFileID', 'kBookmarkFileCreationDate',
12 | 'kBookmarkTOCPath', 'kBookmarkVolumePath',
13 | 'kBookmarkVolumeURL', 'kBookmarkVolumeName', 'kBookmarkVolumeUUID',
14 | 'kBookmarkVolumeSize', 'kBookmarkVolumeCreationDate',
15 | 'kBookmarkVolumeProperties', 'kBookmarkContainingFolder',
16 | 'kBookmarkUserName', 'kBookmarkUID', 'kBookmarkWasFileReference',
17 | 'kBookmarkCreationOptions', 'kBookmarkURLLengths',
18 | 'kBookmarkSecurityExtension',
19 | 'AppleShareInfo',
20 | 'VolumeInfo',
21 | 'TargetInfo',
22 | 'Alias',
23 | 'Bookmark',
24 | 'Data',
25 | 'URL' ]
26 |
27 |
28 |
--------------------------------------------------------------------------------
/mac_alias/alias.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 | from __future__ import division
4 |
5 | import struct
6 | import datetime
7 | import io
8 | import re
9 | import os
10 | import os.path
11 | import stat
12 | import sys
13 | from unicodedata import normalize
14 |
15 | if sys.platform == 'darwin':
16 | from . import osx
17 |
18 | try:
19 | long
20 | except NameError:
21 | long = int
22 |
23 | from .utils import *
24 |
25 | ALIAS_KIND_FILE = 0
26 | ALIAS_KIND_FOLDER = 1
27 |
28 | ALIAS_HFS_VOLUME_SIGNATURE = b'H+'
29 |
30 | ALIAS_FILESYSTEM_UDF = 'UDF (CD/DVD)'
31 | ALIAS_FILESYSTEM_FAT32 = 'FAT32'
32 | ALIAS_FILESYSTEM_EXFAT = 'exFAT'
33 | ALIAS_FILESYSTEM_HFSX = 'HFSX'
34 | ALIAS_FILESYSTEM_HFSPLUS = 'HFS+'
35 | ALIAS_FILESYSTEM_FTP = 'FTP'
36 | ALIAS_FILESYSTEM_NTFS = 'NTFS'
37 | ALIAS_FILESYSTEM_UNKNOWN = 'unknown'
38 |
39 | ALIAS_FIXED_DISK = 0
40 | ALIAS_NETWORK_DISK = 1
41 | ALIAS_400KB_FLOPPY_DISK = 2
42 | ALIAS_800KB_FLOPPY_DISK = 3
43 | ALIAS_1_44MB_FLOPPY_DISK = 4
44 | ALIAS_EJECTABLE_DISK = 5
45 |
46 | ALIAS_NO_CNID = 0xffffffff
47 |
48 | ALIAS_FSTYPE_MAP = {
49 | # Version 2 aliases
50 | b'HX': ALIAS_FILESYSTEM_HFSX,
51 | b'H+': ALIAS_FILESYSTEM_HFSPLUS,
52 |
53 | # Version 3 aliases
54 | b'BDcu': ALIAS_FILESYSTEM_UDF,
55 | b'BDIS': ALIAS_FILESYSTEM_FAT32,
56 | b'BDxF': ALIAS_FILESYSTEM_EXFAT,
57 | b'HX\0\0': ALIAS_FILESYSTEM_HFSX,
58 | b'H+\0\0': ALIAS_FILESYSTEM_HFSPLUS,
59 | b'KG\0\0': ALIAS_FILESYSTEM_FTP,
60 | b'NTcu': ALIAS_FILESYSTEM_NTFS,
61 | }
62 |
63 | def encode_utf8(s):
64 | if isinstance(s, bytes):
65 | return s
66 | return s.encode('utf-8')
67 |
68 | def decode_utf8(s):
69 | if isinstance(s, bytes):
70 | return s.decode('utf-8')
71 | return s
72 |
73 | class AppleShareInfo (object):
74 | def __init__(self, zone=None, server=None, user=None):
75 | #: The AppleShare zone
76 | self.zone = zone
77 | #: The AFP server
78 | self.server = server
79 | #: The username
80 | self.user = user
81 |
82 | def __repr__(self):
83 | return 'AppleShareInfo(%r,%r,%r)' % (self.zone, self.server, self.user)
84 |
85 | class VolumeInfo (object):
86 | def __init__(self, name, creation_date, fs_type, disk_type,
87 | attribute_flags, fs_id, appleshare_info=None,
88 | driver_name=None, posix_path=None, disk_image_alias=None,
89 | dialup_info=None, network_mount_info=None):
90 | #: The name of the volume on which the target resides
91 | self.name = name
92 |
93 | #: The creation date of the target's volume
94 | self.creation_date = creation_date
95 |
96 | #: The filesystem type
97 | #: (for v2 aliases, this is a 2-character code; for v3 aliases, a
98 | #: 4-character code).
99 | self.fs_type = fs_type
100 |
101 | #: The type of disk; should be one of
102 | #:
103 | #: * ALIAS_FIXED_DISK
104 | #: * ALIAS_NETWORK_DISK
105 | #: * ALIAS_400KB_FLOPPY_DISK
106 | #: * ALIAS_800KB_FLOPPY_DISK
107 | #: * ALIAS_1_44MB_FLOPPY_DISK
108 | #: * ALIAS_EJECTABLE_DISK
109 | self.disk_type = disk_type
110 |
111 | #: Filesystem attribute flags (from HFS volume header)
112 | self.attribute_flags = attribute_flags
113 |
114 | #: Filesystem identifier
115 | self.fs_id = fs_id
116 |
117 | #: AppleShare information (for automatic remounting of network shares)
118 | #: *(optional)*
119 | self.appleshare_info = appleshare_info
120 |
121 | #: Driver name (*probably* contains a disk driver name on older Macs)
122 | #: *(optional)*
123 | self.driver_name = driver_name
124 |
125 | #: POSIX path of the mount point of the target's volume
126 | #: *(optional)*
127 | self.posix_path = posix_path
128 |
129 | #: :class:`Alias` object pointing at the disk image on which the
130 | #: target's volume resides *(optional)*
131 | self.disk_image_alias = disk_image_alias
132 |
133 | #: Dialup information (for automatic establishment of dialup connections)
134 | self.dialup_info = dialup_info
135 |
136 | #: Network mount information (for automatic remounting)
137 | self.network_mount_info = network_mount_info
138 |
139 | @property
140 | def filesystem_type(self):
141 | return ALIAS_FSTYPE_MAP.get(self.fs_type, ALIAS_FILESYSTEM_UNKNOWN)
142 |
143 | def __repr__(self):
144 | args = ['name', 'creation_date', 'fs_type', 'disk_type',
145 | 'attribute_flags', 'fs_id']
146 | values = []
147 | for a in args:
148 | v = getattr(self, a)
149 | values.append(repr(v))
150 |
151 | kwargs = ['appleshare_info', 'driver_name', 'posix_path',
152 | 'disk_image_alias', 'dialup_info', 'network_mount_info']
153 | for a in kwargs:
154 | v = getattr(self, a)
155 | if v is not None:
156 | values.append('%s=%r' % (a, v))
157 | return 'VolumeInfo(%s)' % ','.join(values)
158 |
159 | class TargetInfo (object):
160 | def __init__(self, kind, filename, folder_cnid, cnid, creation_date,
161 | creator_code, type_code, levels_from=-1, levels_to=-1,
162 | folder_name=None, cnid_path=None, carbon_path=None,
163 | posix_path=None, user_home_prefix_len=None):
164 | #: Either ALIAS_KIND_FILE or ALIAS_KIND_FOLDER
165 | self.kind = kind
166 |
167 | #: The filename of the target
168 | self.filename = filename
169 |
170 | #: The CNID (Catalog Node ID) of the target's containing folder;
171 | #: CNIDs are similar to but different than traditional UNIX inode
172 | #: numbers
173 | self.folder_cnid = folder_cnid
174 |
175 | #: The CNID (Catalog Node ID) of the target
176 | self.cnid = cnid
177 |
178 | #: The target's *creation* date.
179 | self.creation_date = creation_date
180 |
181 | #: The target's Mac creator code (a four-character binary string)
182 | self.creator_code = creator_code
183 |
184 | #: The target's Mac type code (a four-character binary string)
185 | self.type_code = type_code
186 |
187 | #: The depth of the alias? Always seems to be -1 on OS X.
188 | self.levels_from = levels_from
189 |
190 | #: The depth of the target? Always seems to be -1 on OS X.
191 | self.levels_to = levels_to
192 |
193 | #: The (POSIX) name of the target's containing folder. *(optional)*
194 | self.folder_name = folder_name
195 |
196 | #: The path from the volume root as a sequence of CNIDs. *(optional)*
197 | self.cnid_path = cnid_path
198 |
199 | #: The Carbon path of the target *(optional)*
200 | self.carbon_path = carbon_path
201 |
202 | #: The POSIX path of the target relative to the volume root. Note
203 | #: that this may or may not have a leading '/' character, but it is
204 | #: always relative to the containing volume. *(optional)*
205 | self.posix_path = posix_path
206 |
207 | #: If the path points into a user's home folder, the number of folders
208 | #: deep that we go before we get to that home folder. *(optional)*
209 | self.user_home_prefix_len = user_home_prefix_len
210 |
211 | def __repr__(self):
212 | args = ['kind', 'filename', 'folder_cnid', 'cnid', 'creation_date',
213 | 'creator_code', 'type_code']
214 | values = []
215 | for a in args:
216 | v = getattr(self, a)
217 | values.append(repr(v))
218 |
219 | if self.levels_from != -1:
220 | values.append('levels_from=%r' % self.levels_from)
221 | if self.levels_to != -1:
222 | values.append('levels_to=%r' % self.levels_to)
223 |
224 | kwargs = ['folder_name', 'cnid_path', 'carbon_path',
225 | 'posix_path', 'user_home_prefix_len']
226 | for a in kwargs:
227 | v = getattr(self, a)
228 | values.append('%s=%r' % (a, v))
229 |
230 | return 'TargetInfo(%s)' % ','.join(values)
231 |
232 | TAG_CARBON_FOLDER_NAME = 0
233 | TAG_CNID_PATH = 1
234 | TAG_CARBON_PATH = 2
235 | TAG_APPLESHARE_ZONE = 3
236 | TAG_APPLESHARE_SERVER_NAME = 4
237 | TAG_APPLESHARE_USERNAME = 5
238 | TAG_DRIVER_NAME = 6
239 | TAG_NETWORK_MOUNT_INFO = 9
240 | TAG_DIALUP_INFO = 10
241 | TAG_UNICODE_FILENAME = 14
242 | TAG_UNICODE_VOLUME_NAME = 15
243 | TAG_HIGH_RES_VOLUME_CREATION_DATE = 16
244 | TAG_HIGH_RES_CREATION_DATE = 17
245 | TAG_POSIX_PATH = 18
246 | TAG_POSIX_PATH_TO_MOUNTPOINT = 19
247 | TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE = 20
248 | TAG_USER_HOME_LENGTH_PREFIX = 21
249 |
250 | class Alias (object):
251 | def __init__(self, appinfo=b'\0\0\0\0', version=2, volume=None,
252 | target=None, extra=[]):
253 | """Construct a new :class:`Alias` object with the specified
254 | contents."""
255 |
256 | #: Application specific information (four byte byte-string)
257 | self.appinfo = appinfo
258 |
259 | #: Version (we support versions 2 and 3)
260 | self.version = version
261 |
262 | #: A :class:`VolumeInfo` object describing the target's volume
263 | self.volume = volume
264 |
265 | #: A :class:`TargetInfo` object describing the target
266 | self.target = target
267 |
268 | #: A list of extra `(tag, value)` pairs
269 | self.extra = list(extra)
270 |
271 | @classmethod
272 | def _from_fd(cls, b):
273 | appinfo, recsize, version = struct.unpack(b'>4shh', b.read(8))
274 |
275 | if recsize < 150:
276 | raise ValueError('Incorrect alias length')
277 |
278 | if version not in (2, 3):
279 | raise ValueError('Unsupported alias version %u' % version)
280 |
281 | if version == 2:
282 | kind, volname, voldate, fstype, disktype, \
283 | folder_cnid, filename, cnid, crdate, creator_code, type_code, \
284 | levels_from, levels_to, volattrs, volfsid, reserved = \
285 | struct.unpack(b'>h28pI2shI64pII4s4shhI2s10s', b.read(142))
286 | else:
287 | kind, voldate_hr, fstype, disktype, folder_cnid, cnid, crdate_hr, \
288 | volattrs, reserved = \
289 | struct.unpack(b'>hQ4shIIQI14s', b.read(46))
290 |
291 | volname = b''
292 | filename = b''
293 | creator_code = None
294 | type_code = None
295 | voldate = voldate_hr / 65536.0
296 | crdate = crdate_hr / 65536.0
297 |
298 | voldate = mac_epoch + datetime.timedelta(seconds=voldate)
299 | crdate = mac_epoch + datetime.timedelta(seconds=crdate)
300 |
301 | alias = Alias()
302 | alias.appinfo = appinfo
303 |
304 | alias.volume = VolumeInfo (volname.decode().replace('/',':'),
305 | voldate, fstype, disktype,
306 | volattrs, volfsid)
307 | alias.target = TargetInfo (kind, filename.decode().replace('/',':'),
308 | folder_cnid, cnid,
309 | crdate, creator_code, type_code)
310 | alias.target.levels_from = levels_from
311 | alias.target.levels_to = levels_to
312 |
313 | tag = struct.unpack(b'>h', b.read(2))[0]
314 |
315 | while tag != -1:
316 | length = struct.unpack(b'>h', b.read(2))[0]
317 | value = b.read(length)
318 | if length & 1:
319 | b.read(1)
320 |
321 | if tag == TAG_CARBON_FOLDER_NAME:
322 | alias.target.folder_name = value.decode().replace('/',':')
323 | elif tag == TAG_CNID_PATH:
324 | alias.target.cnid_path = struct.unpack('>%uI' % (length // 4),
325 | value)
326 | elif tag == TAG_CARBON_PATH:
327 | alias.target.carbon_path = value
328 | elif tag == TAG_APPLESHARE_ZONE:
329 | if alias.volume.appleshare_info is None:
330 | alias.volume.appleshare_info = AppleShareInfo()
331 | alias.volume.appleshare_info.zone = value
332 | elif tag == TAG_APPLESHARE_SERVER_NAME:
333 | if alias.volume.appleshare_info is None:
334 | alias.volume.appleshare_info = AppleShareInfo()
335 | alias.volume.appleshare_info.server = value
336 | elif tag == TAG_APPLESHARE_USERNAME:
337 | if alias.volume.appleshare_info is None:
338 | alias.volume.appleshare_info = AppleShareInfo()
339 | alias.volume.appleshare_info.user = value
340 | elif tag == TAG_DRIVER_NAME:
341 | alias.volume.driver_name = value
342 | elif tag == TAG_NETWORK_MOUNT_INFO:
343 | alias.volume.network_mount_info = value
344 | elif tag == TAG_DIALUP_INFO:
345 | alias.volume.dialup_info = value
346 | elif tag == TAG_UNICODE_FILENAME:
347 | alias.target.filename = value[2:].decode('utf-16be')
348 | elif tag == TAG_UNICODE_VOLUME_NAME:
349 | alias.volume.name = value[2:].decode('utf-16be')
350 | elif tag == TAG_HIGH_RES_VOLUME_CREATION_DATE:
351 | seconds = struct.unpack(b'>Q', value)[0] / 65536.0
352 | alias.volume.creation_date \
353 | = mac_epoch + datetime.timedelta(seconds=seconds)
354 | elif tag == TAG_HIGH_RES_CREATION_DATE:
355 | seconds = struct.unpack(b'>Q', value)[0] / 65536.0
356 | alias.target.creation_date \
357 | = mac_epoch + datetime.timedelta(seconds=seconds)
358 | elif tag == TAG_POSIX_PATH:
359 | alias.target.posix_path = value.decode()
360 | elif tag == TAG_POSIX_PATH_TO_MOUNTPOINT:
361 | alias.volume.posix_path = value.decode()
362 | elif tag == TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE:
363 | alias.volume.disk_image_alias = Alias.from_bytes(value)
364 | elif tag == TAG_USER_HOME_LENGTH_PREFIX:
365 | alias.target.user_home_prefix_len = struct.unpack(b'>h', value)[0]
366 | else:
367 | alias.extra.append((tag, value))
368 |
369 | tag = struct.unpack(b'>h', b.read(2))[0]
370 |
371 | return alias
372 |
373 | @classmethod
374 | def from_bytes(cls, bytes):
375 | """Construct an :class:`Alias` object given binary Alias data."""
376 | with io.BytesIO(bytes) as b:
377 | return cls._from_fd(b)
378 |
379 | @classmethod
380 | def for_file(cls, path):
381 | """Create an :class:`Alias` that points at the specified file."""
382 | if sys.platform != 'darwin':
383 | raise Exception('Not implemented (requires special support)')
384 |
385 | path = encode_utf8(path)
386 |
387 | a = Alias()
388 |
389 | # Find the filesystem
390 | st = osx.statfs(path)
391 | vol_path = st.f_mntonname
392 |
393 | # File and folder names in HFS+ are normalized to a form similar to NFD.
394 | # Must be normalized (NFD->NFC) before use to avoid unicode string comparison issues.
395 | vol_path = normalize("NFC", vol_path.decode('utf-8')).encode('utf-8')
396 |
397 | # Grab its attributes
398 | attrs = [osx.ATTR_CMN_CRTIME,
399 | osx.ATTR_VOL_NAME,
400 | 0, 0, 0]
401 | volinfo = osx.getattrlist(vol_path, attrs, 0)
402 |
403 | vol_crtime = volinfo[0]
404 | vol_name = encode_utf8(volinfo[1])
405 |
406 | # Also grab various attributes of the file
407 | attrs = [(osx.ATTR_CMN_OBJTYPE
408 | | osx.ATTR_CMN_CRTIME
409 | | osx.ATTR_CMN_FNDRINFO
410 | | osx.ATTR_CMN_FILEID
411 | | osx.ATTR_CMN_PARENTID), 0, 0, 0, 0]
412 | info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW)
413 |
414 | if info[0] == osx.VDIR:
415 | kind = ALIAS_KIND_FOLDER
416 | else:
417 | kind = ALIAS_KIND_FILE
418 |
419 | cnid = info[3]
420 | folder_cnid = info[4]
421 |
422 | dirname, filename = os.path.split(path)
423 |
424 | if dirname == b'' or dirname == b'.':
425 | dirname = os.getcwd()
426 |
427 | foldername = os.path.basename(dirname)
428 |
429 | creation_date = info[1]
430 |
431 | if kind == ALIAS_KIND_FILE:
432 | creator_code = struct.pack(b'I', info[2].fileInfo.fileCreator)
433 | type_code = struct.pack(b'I', info[2].fileInfo.fileType)
434 | else:
435 | creator_code = b'\0\0\0\0'
436 | type_code = b'\0\0\0\0'
437 |
438 | a.target = TargetInfo(kind, filename, folder_cnid, cnid, creation_date,
439 | creator_code, type_code)
440 | a.volume = VolumeInfo(vol_name, vol_crtime, b'H+',
441 | ALIAS_FIXED_DISK, 0, b'\0\0')
442 |
443 | a.target.folder_name = foldername
444 | a.volume.posix_path = vol_path
445 |
446 | rel_path = os.path.relpath(path, vol_path)
447 |
448 | # Leave off the initial '/' if vol_path is '/' (no idea why)
449 | if vol_path == b'/':
450 | a.target.posix_path = rel_path
451 | else:
452 | a.target.posix_path = b'/' + rel_path
453 |
454 | # Construct the Carbon and CNID paths
455 | carbon_path = []
456 | cnid_path = []
457 | head, tail = os.path.split(rel_path)
458 | if not tail:
459 | head, tail = os.path.split(head)
460 | while head or tail:
461 | if head:
462 | attrs = [osx.ATTR_CMN_FILEID, 0, 0, 0, 0]
463 | info = osx.getattrlist(os.path.join(vol_path, head), attrs, 0)
464 | cnid_path.append(info[0])
465 | carbon_tail = tail.replace(b':',b'/')
466 | carbon_path.insert(0, carbon_tail)
467 | head, tail = os.path.split(head)
468 |
469 | carbon_path = vol_name + b':' + b':\0'.join(carbon_path)
470 |
471 | a.target.carbon_path = carbon_path
472 | a.target.cnid_path = cnid_path
473 |
474 | return a
475 |
476 | def _to_fd(self, b):
477 | # We'll come back and fix the length when we're done
478 | pos = b.tell()
479 | b.write(struct.pack(b'>4shh', self.appinfo, 0, self.version))
480 |
481 | carbon_volname = encode_utf8(self.volume.name).replace(b':',b'/')
482 | carbon_filename = encode_utf8(self.target.filename).replace(b':',b'/')
483 | voldate = (self.volume.creation_date - mac_epoch).total_seconds()
484 | crdate = (self.target.creation_date - mac_epoch).total_seconds()
485 |
486 | if self.version == 2:
487 | # NOTE: crdate should be in local time, but that's system dependent
488 | # (so doing so is ridiculous, and nothing could rely on it).
489 | b.write(struct.pack(b'>h28pI2shI64pII4s4shhI2s10s',
490 | self.target.kind,
491 | carbon_volname, int(voldate),
492 | self.volume.fs_type,
493 | self.volume.disk_type,
494 | self.target.folder_cnid,
495 | carbon_filename,
496 | self.target.cnid,
497 | int(crdate),
498 | self.target.creator_code,
499 | self.target.type_code,
500 | self.target.levels_from,
501 | self.target.levels_to,
502 | self.volume.attribute_flags,
503 | self.volume.fs_id,
504 | b'\0'*10))
505 | else:
506 | b.write(struct.pack(b'>hQ4shIIQI14s',
507 | self.target.kind,
508 | int(voldate * 65536),
509 | self.volume.fs_type,
510 | self.volume.disk_type,
511 | self.target.folder_cnid,
512 | self.target.cnid,
513 | int(crdate * 65536),
514 | self.volume.attribute_flags,
515 | self.volume.fs_id,
516 | b'\0'*14))
517 |
518 | # Excuse the odd order; we're copying Finder
519 | if self.target.folder_name:
520 | carbon_foldername = encode_utf8(self.target.folder_name)\
521 | .replace(b':',b'/')
522 | b.write(struct.pack(b'>hh', TAG_CARBON_FOLDER_NAME,
523 | len(carbon_foldername)))
524 | b.write(carbon_foldername)
525 | if len(carbon_foldername) & 1:
526 | b.write(b'\0')
527 |
528 | b.write(struct.pack(b'>hhQhhQ',
529 | TAG_HIGH_RES_VOLUME_CREATION_DATE,
530 | 8, int(voldate * 65536),
531 | TAG_HIGH_RES_CREATION_DATE,
532 | 8, int(crdate * 65536)))
533 |
534 | if self.target.cnid_path:
535 | cnid_path = struct.pack('>%uI' % len(self.target.cnid_path),
536 | *self.target.cnid_path)
537 | b.write(struct.pack(b'>hh', TAG_CNID_PATH,
538 | len(cnid_path)))
539 | b.write(cnid_path)
540 |
541 | if self.target.carbon_path:
542 | carbon_path=encode_utf8(self.target.carbon_path)
543 | b.write(struct.pack(b'>hh', TAG_CARBON_PATH,
544 | len(carbon_path)))
545 | b.write(carbon_path)
546 | if len(carbon_path) & 1:
547 | b.write(b'\0')
548 |
549 | if self.volume.appleshare_info:
550 | ai = self.volume.appleshare_info
551 | if ai.zone:
552 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_ZONE,
553 | len(ai.zone)))
554 | b.write(ai.zone)
555 | if len(ai.zone) & 1:
556 | b.write(b'\0')
557 | if ai.server:
558 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_SERVER_NAME,
559 | len(ai.server)))
560 | b.write(ai.server)
561 | if len(ai.server) & 1:
562 | b.write(b'\0')
563 | if ai.username:
564 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_USERNAME,
565 | len(ai.username)))
566 | b.write(ai.username)
567 | if len(ai.username) & 1:
568 | b.write(b'\0')
569 |
570 | if self.volume.driver_name:
571 | driver_name = encode_utf8(self.volume.driver_name)
572 | b.write(struct.pack(b'>hh', TAG_DRIVER_NAME,
573 | len(driver_name)))
574 | b.write(driver_name)
575 | if len(driver_name) & 1:
576 | b.write(b'\0')
577 |
578 | if self.volume.network_mount_info:
579 | b.write(struct.pack(b'>hh', TAG_NETWORK_MOUNT_INFO,
580 | len(self.volume.network_mount_info)))
581 | b.write(self.volume.network_mount_info)
582 | if len(self.volume.network_mount_info) & 1:
583 | b.write(b'\0')
584 |
585 | if self.volume.dialup_info:
586 | b.write(struct.pack(b'>hh', TAG_DIALUP_INFO,
587 | len(self.volume.network_mount_info)))
588 | b.write(self.volume.network_mount_info)
589 | if len(self.volume.network_mount_info) & 1:
590 | b.write(b'\0')
591 |
592 | utf16 = decode_utf8(self.target.filename)\
593 | .replace(':','/').encode('utf-16-be')
594 | b.write(struct.pack(b'>hhh', TAG_UNICODE_FILENAME,
595 | len(utf16) + 2,
596 | len(utf16) // 2))
597 | b.write(utf16)
598 |
599 | utf16 = decode_utf8(self.volume.name)\
600 | .replace(':','/').encode('utf-16-be')
601 | b.write(struct.pack(b'>hhh', TAG_UNICODE_VOLUME_NAME,
602 | len(utf16) + 2,
603 | len(utf16) // 2))
604 | b.write(utf16)
605 |
606 | if self.target.posix_path:
607 | posix_path = encode_utf8(self.target.posix_path)
608 | b.write(struct.pack(b'>hh', TAG_POSIX_PATH,
609 | len(posix_path)))
610 | b.write(posix_path)
611 | if len(posix_path) & 1:
612 | b.write(b'\0')
613 |
614 | if self.volume.posix_path:
615 | posix_path = encode_utf8(self.volume.posix_path)
616 | b.write(struct.pack(b'>hh', TAG_POSIX_PATH_TO_MOUNTPOINT,
617 | len(posix_path)))
618 | b.write(posix_path)
619 | if len(posix_path) & 1:
620 | b.write(b'\0')
621 |
622 | if self.volume.disk_image_alias:
623 | d = self.volume.disk_image_alias.to_bytes()
624 | b.write(struct.pack(b'>hh', TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE,
625 | len(d)))
626 | b.write(d)
627 | if len(d) & 1:
628 | b.write(b'\0')
629 |
630 | if self.target.user_home_prefix_len is not None:
631 | b.write(struct.pack(b'>hhh', TAG_USER_HOME_LENGTH_PREFIX,
632 | 2, self.target.user_home_prefix_len))
633 |
634 | for t,v in self.extra:
635 | b.write(struct.pack(b'>hh', t, len(v)))
636 | b.write(v)
637 | if len(v) & 1:
638 | b.write(b'\0')
639 |
640 | b.write(struct.pack(b'>hh', -1, 0))
641 |
642 | blen = b.tell() - pos
643 | b.seek(pos + 4, os.SEEK_SET)
644 | b.write(struct.pack(b'>h', blen))
645 |
646 | def to_bytes(self):
647 | """Returns the binary representation for this :class:`Alias`."""
648 | with io.BytesIO() as b:
649 | self._to_fd(b)
650 | return b.getvalue()
651 |
652 | def __str__(self):
653 | return '' % self.target.filename
654 |
655 | def __repr__(self):
656 | values = []
657 | if self.appinfo != b'\0\0\0\0':
658 | values.append('appinfo=%r' % self.appinfo)
659 | if self.version != 2:
660 | values.append('version=%r' % self.version)
661 | if self.volume is not None:
662 | values.append('volume=%r' % self.volume)
663 | if self.target is not None:
664 | values.append('target=%r' % self.target)
665 | if self.extra:
666 | values.append('extra=%r' % self.extra)
667 | return 'Alias(%s)' % ','.join(values)
668 |
--------------------------------------------------------------------------------
/mac_alias/bookmark.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # This file implements the Apple "bookmark" format, which is the replacement
4 | # for the old-fashioned alias format. The details of this format were
5 | # reverse engineered; some things are still not entirely clear.
6 | #
7 | from __future__ import unicode_literals, print_function
8 |
9 | import struct
10 | import uuid
11 | import datetime
12 | import os
13 | import sys
14 | import pprint
15 |
16 | try:
17 | from urlparse import urljoin
18 | except ImportError:
19 | from urllib.parse import urljoin
20 |
21 | if sys.platform == 'darwin':
22 | from . import osx
23 |
24 | def iteritems(x):
25 | return x.iteritems()
26 |
27 | try:
28 | unicode
29 | except NameError:
30 | unicode = str
31 | long = int
32 | xrange = range
33 | def iteritems(x):
34 | return x.items()
35 |
36 | from .utils import *
37 |
38 | BMK_DATA_TYPE_MASK = 0xffffff00
39 | BMK_DATA_SUBTYPE_MASK = 0x000000ff
40 |
41 | BMK_STRING = 0x0100
42 | BMK_DATA = 0x0200
43 | BMK_NUMBER = 0x0300
44 | BMK_DATE = 0x0400
45 | BMK_BOOLEAN = 0x0500
46 | BMK_ARRAY = 0x0600
47 | BMK_DICT = 0x0700
48 | BMK_UUID = 0x0800
49 | BMK_URL = 0x0900
50 | BMK_NULL = 0x0a00
51 |
52 | BMK_ST_ZERO = 0x0000
53 | BMK_ST_ONE = 0x0001
54 |
55 | BMK_BOOLEAN_ST_FALSE = 0x0000
56 | BMK_BOOLEAN_ST_TRUE = 0x0001
57 |
58 | # Subtypes for BMK_NUMBER are really CFNumberType values
59 | kCFNumberSInt8Type = 1
60 | kCFNumberSInt16Type = 2
61 | kCFNumberSInt32Type = 3
62 | kCFNumberSInt64Type = 4
63 | kCFNumberFloat32Type = 5
64 | kCFNumberFloat64Type = 6
65 | kCFNumberCharType = 7
66 | kCFNumberShortType = 8
67 | kCFNumberIntType = 9
68 | kCFNumberLongType = 10
69 | kCFNumberLongLongType = 11
70 | kCFNumberFloatType = 12
71 | kCFNumberDoubleType = 13
72 | kCFNumberCFIndexType = 14
73 | kCFNumberNSIntegerType = 15
74 | kCFNumberCGFloatType = 16
75 |
76 | # Resource property flags (from CFURLPriv.h)
77 | kCFURLResourceIsRegularFile = 0x00000001
78 | kCFURLResourceIsDirectory = 0x00000002
79 | kCFURLResourceIsSymbolicLink = 0x00000004
80 | kCFURLResourceIsVolume = 0x00000008
81 | kCFURLResourceIsPackage = 0x00000010
82 | kCFURLResourceIsSystemImmutable = 0x00000020
83 | kCFURLResourceIsUserImmutable = 0x00000040
84 | kCFURLResourceIsHidden = 0x00000080
85 | kCFURLResourceHasHiddenExtension = 0x00000100
86 | kCFURLResourceIsApplication = 0x00000200
87 | kCFURLResourceIsCompressed = 0x00000400
88 | kCFURLResourceIsSystemCompressed = 0x00000400
89 | kCFURLCanSetHiddenExtension = 0x00000800
90 | kCFURLResourceIsReadable = 0x00001000
91 | kCFURLResourceIsWriteable = 0x00002000
92 | kCFURLResourceIsExecutable = 0x00004000
93 | kCFURLIsAliasFile = 0x00008000
94 | kCFURLIsMountTrigger = 0x00010000
95 |
96 | # Volume property flags (from CFURLPriv.h)
97 | kCFURLVolumeIsLocal = 0x1 #
98 | kCFURLVolumeIsAutomount = 0x2 #
99 | kCFURLVolumeDontBrowse = 0x4 #
100 | kCFURLVolumeIsReadOnly = 0x8 #
101 | kCFURLVolumeIsQuarantined = 0x10
102 | kCFURLVolumeIsEjectable = 0x20 #
103 | kCFURLVolumeIsRemovable = 0x40 #
104 | kCFURLVolumeIsInternal = 0x80 #
105 | kCFURLVolumeIsExternal = 0x100 #
106 | kCFURLVolumeIsDiskImage = 0x200 #
107 | kCFURLVolumeIsFileVault = 0x400
108 | kCFURLVolumeIsLocaliDiskMirror = 0x800
109 | kCFURLVolumeIsiPod = 0x1000 #
110 | kCFURLVolumeIsiDisk = 0x2000
111 | kCFURLVolumeIsCD = 0x4000
112 | kCFURLVolumeIsDVD = 0x8000
113 | kCFURLVolumeIsDeviceFileSystem = 0x10000
114 | kCFURLVolumeSupportsPersistentIDs = 0x100000000
115 | kCFURLVolumeSupportsSearchFS = 0x200000000
116 | kCFURLVolumeSupportsExchange = 0x400000000
117 | # reserved 0x800000000
118 | kCFURLVolumeSupportsSymbolicLinks = 0x1000000000
119 | kCFURLVolumeSupportsDenyModes = 0x2000000000
120 | kCFURLVolumeSupportsCopyFile = 0x4000000000
121 | kCFURLVolumeSupportsReadDirAttr = 0x8000000000
122 | kCFURLVolumeSupportsJournaling = 0x10000000000
123 | kCFURLVolumeSupportsRename = 0x20000000000
124 | kCFURLVolumeSupportsFastStatFS = 0x40000000000
125 | kCFURLVolumeSupportsCaseSensitiveNames = 0x80000000000
126 | kCFURLVolumeSupportsCasePreservedNames = 0x100000000000
127 | kCFURLVolumeSupportsFLock = 0x200000000000
128 | kCFURLVolumeHasNoRootDirectoryTimes = 0x400000000000
129 | kCFURLVolumeSupportsExtendedSecurity = 0x800000000000
130 | kCFURLVolumeSupports2TBFileSize = 0x1000000000000
131 | kCFURLVolumeSupportsHardLinks = 0x2000000000000
132 | kCFURLVolumeSupportsMandatoryByteRangeLocks = 0x4000000000000
133 | kCFURLVolumeSupportsPathFromID = 0x8000000000000
134 | # reserved 0x10000000000000
135 | kCFURLVolumeIsJournaling = 0x20000000000000
136 | kCFURLVolumeSupportsSparseFiles = 0x40000000000000
137 | kCFURLVolumeSupportsZeroRuns = 0x80000000000000
138 | kCFURLVolumeSupportsVolumeSizes = 0x100000000000000
139 | kCFURLVolumeSupportsRemoteEvents = 0x200000000000000
140 | kCFURLVolumeSupportsHiddenFiles = 0x400000000000000
141 | kCFURLVolumeSupportsDecmpFSCompression = 0x800000000000000
142 | kCFURLVolumeHas64BitObjectIDs = 0x1000000000000000
143 | kCFURLVolumePropertyFlagsAll = 0xffffffffffffffff
144 |
145 | BMK_URL_ST_ABSOLUTE = 0x0001
146 | BMK_URL_ST_RELATIVE = 0x0002
147 |
148 | # Bookmark keys
149 | kBookmarkURL = 0x1003 # A URL
150 | kBookmarkPath = 0x1004 # Array of path components
151 | kBookmarkCNIDPath = 0x1005 # Array of CNIDs
152 | kBookmarkFileProperties = 0x1010 # (CFURL rp flags,
153 | # CFURL rp flags asked for,
154 | # 8 bytes NULL)
155 | kBookmarkFileName = 0x1020
156 | kBookmarkFileID = 0x1030
157 | kBookmarkFileCreationDate = 0x1040
158 | # = 0x1054 # ?
159 | # = 0x1055 # ?
160 | # = 0x1056 # ?
161 | # = 0x1101 # ?
162 | # = 0x1102 # ?
163 | kBookmarkTOCPath = 0x2000 # A list of (TOC id, ?) pairs
164 | kBookmarkVolumePath = 0x2002
165 | kBookmarkVolumeURL = 0x2005
166 | kBookmarkVolumeName = 0x2010
167 | kBookmarkVolumeUUID = 0x2011 # Stored (perversely) as a string
168 | kBookmarkVolumeSize = 0x2012
169 | kBookmarkVolumeCreationDate = 0x2013
170 | kBookmarkVolumeProperties = 0x2020 # (CFURL vp flags,
171 | # CFURL vp flags asked for,
172 | # 8 bytes NULL)
173 | kBookmarkVolumeIsRoot = 0x2030 # True if volume is FS root
174 | kBookmarkVolumeBookmark = 0x2040 # Embedded bookmark for disk image (TOC id)
175 | kBookmarkVolumeMountPoint = 0x2050 # A URL
176 | # = 0x2070
177 | kBookmarkContainingFolder = 0xc001 # Index of containing folder in path
178 | kBookmarkUserName = 0xc011 # User that created bookmark
179 | kBookmarkUID = 0xc012 # UID that created bookmark
180 | kBookmarkWasFileReference = 0xd001 # True if the URL was a file reference
181 | kBookmarkCreationOptions = 0xd010
182 | kBookmarkURLLengths = 0xe003 # See below
183 | kBookmarkDisplayName = 0xf017
184 | kBookmarkIconData = 0xf020
185 | kBookmarkIconRef = 0xf021
186 | kBookmarkTypeBindingData = 0xf022
187 | kBookmarkCreationTime = 0xf030
188 | kBookmarkSandboxRwExtension = 0xf080
189 | kBookmarkSandboxRoExtension = 0xf081
190 | kBookmarkAliasData = 0xfe00
191 |
192 | # Alias for backwards compatibility
193 | kBookmarkSecurityExtension = kBookmarkSandboxRwExtension
194 |
195 | # kBookmarkURLLengths is an array that is set if the URL encoded by the
196 | # bookmark had a base URL; in that case, each entry is the length of the
197 | # base URL in question. Thus a URL
198 | #
199 | # file:///foo/bar/baz blam/blat.html
200 | #
201 | # will result in [3, 2], while the URL
202 | #
203 | # file:///foo bar/baz blam blat.html
204 | #
205 | # would result in [1, 2, 1, 1]
206 |
207 |
208 | class Data (object):
209 | def __init__(self, bytedata=None):
210 | #: The bytes, stored as a byte string
211 | self.bytes = bytes(bytedata)
212 |
213 | def __repr__(self):
214 | return 'Data(%r)' % self.bytes
215 |
216 | class URL (object):
217 | def __init__(self, base, rel=None):
218 | if rel is not None:
219 | #: The base URL, if any (a :class:`URL`)
220 | self.base = base
221 | #: The rest of the URL (a string)
222 | self.relative = rel
223 | else:
224 | self.base = None
225 | self.relative = base
226 |
227 | @property
228 | def absolute(self):
229 | """Return an absolute URL."""
230 | if self.base is None:
231 | return self.relative
232 | else:
233 | base_abs = self.base.absolute
234 | return urljoin(self.base.absolute, self.relative)
235 |
236 | def __repr__(self):
237 | return 'URL(%r)' % self.absolute
238 |
239 | class Bookmark (object):
240 | def __init__(self, tocs=None):
241 | if tocs is None:
242 | #: The TOCs for this Bookmark
243 | self.tocs = []
244 | else:
245 | self.tocs = tocs
246 |
247 | @classmethod
248 | def _get_item(cls, data, hdrsize, offset):
249 | offset += hdrsize
250 | if offset > len(data) - 8:
251 | raise ValueError('Offset out of range')
252 |
253 | length,typecode = struct.unpack(b'd', databytes)[0])
284 | return osx_epoch + secs
285 | elif dtype == BMK_BOOLEAN:
286 | if dsubtype == BMK_BOOLEAN_ST_TRUE:
287 | return True
288 | elif dsubtype == BMK_BOOLEAN_ST_FALSE:
289 | return False
290 | elif dtype == BMK_UUID:
291 | return uuid.UUID(bytes=databytes)
292 | elif dtype == BMK_URL:
293 | if dsubtype == BMK_URL_ST_ABSOLUTE:
294 | return URL(databytes.decode('utf-8'))
295 | elif dsubtype == BMK_URL_ST_RELATIVE:
296 | baseoff,reloff = struct.unpack(b' size:
339 | raise ValueError('Not a bookmark file (header size too large)')
340 |
341 | if size != len(data):
342 | raise ValueError('Not a bookmark file (truncated)')
343 |
344 | tocoffset, = struct.unpack(b' size - hdrsize \
351 | or size - tocbase < 20:
352 | raise ValueError('TOC offset out of range')
353 |
354 | tocsize,tocmagic,tocid,nexttoc,toccount \
355 | = struct.unpack(b' -0x80000000 and item < 0x7fffffff:
428 | result = struct.pack(b'd', float(secs.total_seconds()))
440 | elif isinstance(item, uuid.UUID):
441 | result = struct.pack(b' -1:
512 | sz = sz[:nul]
513 | return sz.decode('utf-8')
514 |
515 | def _decode_attrlist_result(buf, attrs, options):
516 | result = []
517 |
518 | assert len(buf) >= 4
519 | total_size = uint32_t.from_buffer(buf, 0).value
520 | assert total_size <= len(buf)
521 |
522 | offset = 4
523 |
524 | # Common attributes
525 | if attrs[0] & ATTR_CMN_RETURNED_ATTRS:
526 | a = attribute_set_t.from_buffer(buf, offset)
527 | result.append(a)
528 | offset += sizeof (attribute_set_t)
529 | if not (options & FSOPT_PACK_INVAL_ATTRS):
530 | attrs = [a.commonattr, a.volattr, a.dirattr, a.fileattr, a.forkattr]
531 | if attrs[0] & ATTR_CMN_NAME:
532 | a = attrreference_t.from_buffer(buf, offset)
533 | ofs = offset + a.attr_dataoffset
534 | name = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
535 | offset += sizeof (attrreference_t)
536 | result.append(name)
537 | if attrs[0] & ATTR_CMN_DEVID:
538 | a = dev_t.from_buffer(buf, offset)
539 | offset += sizeof(dev_t)
540 | result.append(a.value)
541 | if attrs[0] & ATTR_CMN_FSID:
542 | a = fsid_t.from_buffer(buf, offset)
543 | offset += sizeof(fsid_t)
544 | result.append(a)
545 | if attrs[0] & ATTR_CMN_OBJTYPE:
546 | a = fsobj_type_t.from_buffer(buf, offset)
547 | offset += sizeof(fsobj_type_t)
548 | result.append(a.value)
549 | if attrs[0] & ATTR_CMN_OBJTAG:
550 | a = fsobj_tag_t.from_buffer(buf, offset)
551 | offset += sizeof(fsobj_tag_t)
552 | result.append(a.value)
553 | if attrs[0] & ATTR_CMN_OBJID:
554 | a = fsobj_id_t.from_buffer(buf, offset)
555 | offset += sizeof(fsobj_id_t)
556 | result.append(a)
557 | if attrs[0] & ATTR_CMN_OBJPERMANENTID:
558 | a = fsobj_id_t.from_buffer(buf, offset)
559 | offset += sizeof(fsobj_id_t)
560 | result.append(a)
561 | if attrs[0] & ATTR_CMN_PAROBJID:
562 | a = fsobj_id_t.from_buffer(buf, offset)
563 | offset += sizeof(fsobj_id_t)
564 | result.append(a)
565 | if attrs[0] & ATTR_CMN_SCRIPT:
566 | a = text_encoding_t.from_buffer(buf, offset)
567 | offset += sizeof(text_encoding_t)
568 | result.append(a.value)
569 | if attrs[0] & ATTR_CMN_CRTIME:
570 | a = timespec.from_buffer(buf, offset)
571 | offset += sizeof(timespec)
572 | result.append(_datetime_from_timespec(a))
573 | if attrs[0] & ATTR_CMN_MODTIME:
574 | a = timespec.from_buffer(buf, offset)
575 | offset += sizeof(timespec)
576 | result.append(_datetime_from_timespec(a))
577 | if attrs[0] & ATTR_CMN_CHGTIME:
578 | a = timespec.from_buffer(buf, offset)
579 | offset += sizeof(timespec)
580 | result.append(_datetime_from_timespec(a))
581 | if attrs[0] & ATTR_CMN_ACCTIME:
582 | a = timespec.from_buffer(buf, offset)
583 | offset += sizeof(timespec)
584 | result.append(_datetime_from_timespec(a))
585 | if attrs[0] & ATTR_CMN_BKUPTIME:
586 | a = timespec.from_buffer(buf, offset)
587 | offset += sizeof(timespec)
588 | result.append(_datetime_from_timespec(a))
589 | if attrs[0] & ATTR_CMN_FNDRINFO:
590 | a = FinderInfo.from_buffer(buf, offset)
591 | offset += sizeof(FinderInfo)
592 | result.append(a)
593 | if attrs[0] & ATTR_CMN_OWNERID:
594 | a = uid_t.from_buffer(buf, offset)
595 | offset += sizeof(uid_t)
596 | result.append(a.value)
597 | if attrs[0] & ATTR_CMN_GRPID:
598 | a = gid_t.from_buffer(buf, offset)
599 | offset += sizeof(gid_t)
600 | result.append(a.value)
601 | if attrs[0] & ATTR_CMN_ACCESSMASK:
602 | a = uint32_t.from_buffer(buf, offset)
603 | offset += sizeof(uint32_t)
604 | result.append(a.value)
605 | if attrs[0] & ATTR_CMN_FLAGS:
606 | a = uint32_t.from_buffer(buf, offset)
607 | offset += sizeof(uint32_t)
608 | result.append(a.value)
609 | if attrs[0] & ATTR_CMN_GEN_COUNT:
610 | a = uint32_t.from_buffer(buf, offset)
611 | offset += sizeof(uint32_t)
612 | result.append(a.value)
613 | if attrs[0] & ATTR_CMN_DOCUMENT_ID:
614 | a = uint32_t.from_buffer(buf, offset)
615 | offset += sizeof(uint32_t)
616 | result.append(a.value)
617 | if attrs[0] & ATTR_CMN_USERACCESS:
618 | a = uint32_t.from_buffer(buf, offset)
619 | offset += sizeof(uint32_t)
620 | result.append(a.value)
621 | if attrs[0] & ATTR_CMN_EXTENDED_SECURITY:
622 | a = attrreference_t.from_buffer(buf, offset)
623 | ofs = offset + a.attr_dataoffset
624 | offset += sizeof(attrreference_t)
625 | ec = uint32_t.from_buffer(buf, ofs + 36).value
626 | class kauth_acl(Structure):
627 | _fields_ = [('acl_entrycount', c_uint),
628 | ('acl_flags', c_uint),
629 | ('acl_ace', kauth_ace * ec)]
630 | class kauth_filesec(Structure):
631 | _fields_ = [('fsec_magic', c_uint),
632 | ('fsec_owner', guid_t),
633 | ('fsec_group', guid_t),
634 | ('fsec_acl', kauth_acl)]
635 | a = kauth_filesec.from_buffer(buf, ofs)
636 | result.append(a)
637 | if attrs[0] & ATTR_CMN_UUID:
638 | result.append(uuid.UUID(bytes=buf[offset:offset+16]))
639 | offset += sizeof(guid_t)
640 | if attrs[0] & ATTR_CMN_GRPUUID:
641 | result.append(uuid.UUID(bytes=buf[offset:offset+16]))
642 | offset += sizeof(guid_t)
643 | if attrs[0] & ATTR_CMN_FILEID:
644 | a = uint64_t.from_buffer(buf, offset)
645 | offset += sizeof(uint64_t)
646 | result.append(a.value)
647 | if attrs[0] & ATTR_CMN_PARENTID:
648 | a = uint64_t.from_buffer(buf, offset)
649 | offset += sizeof(uint64_t)
650 | result.append(a.value)
651 | if attrs[0] & ATTR_CMN_FULLPATH:
652 | a = attrreference_t.from_buffer(buf, offset)
653 | ofs = offset + a.attr_dataoffset
654 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
655 | offset += sizeof (attrreference_t)
656 | result.append(path)
657 | if attrs[0] & ATTR_CMN_ADDEDTIME:
658 | a = timespec.from_buffer(buf, offset)
659 | offset += sizeof(timespec)
660 | result.append(_datetime_from_timespec(a))
661 | if attrs[0] & ATTR_CMN_DATA_PROTECT_FLAGS:
662 | a = uint32_t.from_buffer(buf, offset)
663 | offset += sizeof(uint32_t)
664 | result.append(a.value)
665 |
666 | # Volume attributes
667 | if attrs[1] & ATTR_VOL_FSTYPE:
668 | a = uint32_t.from_buffer(buf, offset)
669 | offset += sizeof(uint32_t)
670 | result.append(a.value)
671 | if attrs[1] & ATTR_VOL_SIGNATURE:
672 | a = uint32_t.from_buffer(buf, offset)
673 | offset += sizeof(uint32_t)
674 | result.append(a.value)
675 | if attrs[1] & ATTR_VOL_SIZE:
676 | a = off_t.from_buffer(buf, offset)
677 | offset += sizeof(off_t)
678 | result.append(a.value)
679 | if attrs[1] & ATTR_VOL_SPACEFREE:
680 | a = off_t.from_buffer(buf, offset)
681 | offset += sizeof(off_t)
682 | result.append(a.value)
683 | if attrs[1] & ATTR_VOL_SPACEAVAIL:
684 | a = off_t.from_buffer(buf, offset)
685 | offset += sizeof(off_t)
686 | result.append(a.value)
687 | if attrs[1] & ATTR_VOL_MINALLOCATION:
688 | a = off_t.from_buffer(buf, offset)
689 | offset += sizeof(off_t)
690 | result.append(a.value)
691 | if attrs[1] & ATTR_VOL_ALLOCATIONCLUMP:
692 | a = off_t.from_buffer(buf, offset)
693 | offset += sizeof(off_t)
694 | result.append(a.value)
695 | if attrs[1] & ATTR_VOL_IOBLOCKSIZE:
696 | a = uint32_t.from_buffer(buf, offset)
697 | offset += sizeof(uint32_t)
698 | result.append(a.value)
699 | if attrs[1] & ATTR_VOL_OBJCOUNT:
700 | a = uint32_t.from_buffer(buf, offset)
701 | offset += sizeof(uint32_t)
702 | result.append(a.value)
703 | if attrs[1] & ATTR_VOL_FILECOUNT:
704 | a = uint32_t.from_buffer(buf, offset)
705 | offset += sizeof(uint32_t)
706 | result.append(a.value)
707 | if attrs[1] & ATTR_VOL_DIRCOUNT:
708 | a = uint32_t.from_buffer(buf, offset)
709 | offset += sizeof(uint32_t)
710 | result.append(a.value)
711 | if attrs[1] & ATTR_VOL_MAXOBJCOUNT:
712 | a = uint32_t.from_buffer(buf, offset)
713 | offset += sizeof(uint32_t)
714 | result.append(a.value)
715 | if attrs[1] & ATTR_VOL_MOUNTPOINT:
716 | a = attrreference_t.from_buffer(buf, offset)
717 | ofs = offset + a.attr_dataoffset
718 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
719 | offset += sizeof (attrreference_t)
720 | result.append(path)
721 | if attrs[1] & ATTR_VOL_NAME:
722 | a = attrreference_t.from_buffer(buf, offset)
723 | ofs = offset + a.attr_dataoffset
724 | name = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
725 | offset += sizeof (attrreference_t)
726 | result.append(name)
727 | if attrs[1] & ATTR_VOL_MOUNTFLAGS:
728 | a = uint32_t.from_buffer(buf, offset)
729 | offset += sizeof(uint32_t)
730 | result.append(a.value)
731 | if attrs[1] & ATTR_VOL_MOUNTEDDEVICE:
732 | a = attrreference_t.from_buffer(buf, offset)
733 | ofs = offset + a.attr_dataoffset
734 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
735 | offset += sizeof (attrreference_t)
736 | result.append(path)
737 | if attrs[1] & ATTR_VOL_ENCODINGSUSED:
738 | a = c_ulonglong.from_buffer(buf, offset)
739 | offset += sizeof(c_ulonglong)
740 | result.append(a.value)
741 | if attrs[1] & ATTR_VOL_CAPABILITIES:
742 | a = vol_capabilities_attr_t.from_buffer(buf, offset)
743 | offset += sizeof(vol_capabilities_attr_t)
744 | result.append(a)
745 | if attrs[1] & ATTR_VOL_UUID:
746 | result.append(uuid.UUID(bytes=buf[offset:offset+16]))
747 | offset += sizeof(uuid_t)
748 | if attrs[1] & ATTR_VOL_QUOTA_SIZE:
749 | a = off_t.from_buffer(buf, offset)
750 | offset += sizeof(off_t)
751 | result.append(a.value)
752 | if attrs[1] & ATTR_VOL_RESERVED_SIZE:
753 | a = off_t.from_buffer(buf, offset)
754 | offset += sizeof(off_t)
755 | result.append(a.value)
756 | if attrs[1] & ATTR_VOL_ATTRIBUTES:
757 | a = vol_attributes_attr_t.from_buffer(buf, offset)
758 | offset += sizeof(vol_attributes_attr_t)
759 | result.append(a)
760 |
761 | # Directory attributes
762 | if attrs[2] & ATTR_DIR_LINKCOUNT:
763 | a = uint32_t.from_buffer(buf, offset)
764 | offset += sizeof(uint32_t)
765 | result.append(a.value)
766 | if attrs[2] & ATTR_DIR_ENTRYCOUNT:
767 | a = uint32_t.from_buffer(buf, offset)
768 | offset += sizeof(uint32_t)
769 | result.append(a.value)
770 | if attrs[2] & ATTR_DIR_MOUNTSTATUS:
771 | a = uint32_t.from_buffer(buf, offset)
772 | offset += sizeof(uint32_t)
773 | result.append(a.value)
774 | if attrs[2] & ATTR_DIR_ALLOCSIZE:
775 | a = off_t.from_buffer(buf, offset)
776 | offset += sizeof(off_t)
777 | result.append(a.value)
778 | if attrs[2] & ATTR_DIR_IOBLOCKSIZE:
779 | a = uint32_t.from_buffer(buf, offset)
780 | offset += sizeof(uint32_t)
781 | result.append(a.value)
782 | if attrs[2] & ATTR_DIR_DATALENGTH:
783 | a = off_t.from_buffer(buf, offset)
784 | offset += sizeof(off_t)
785 | result.append(a.value)
786 |
787 | # File attributes
788 | if attrs[3] & ATTR_FILE_LINKCOUNT:
789 | a = uint32_t.from_buffer(buf, offset)
790 | offset += sizeof(uint32_t)
791 | result.append(a.value)
792 | if attrs[3] & ATTR_FILE_TOTALSIZE:
793 | a = off_t.from_buffer(buf, offset)
794 | offset += sizeof(off_t)
795 | result.append(a.value)
796 | if attrs[3] & ATTR_FILE_ALLOCSIZE:
797 | a = off_t.from_buffer(buf, offset)
798 | offset += sizeof(off_t)
799 | result.append(a.value)
800 | if attrs[3] & ATTR_FILE_IOBLOCKSIZE:
801 | a = uint32_t.from_buffer(buf, offset)
802 | offset += sizeof(uint32_t)
803 | result.append(a.value)
804 | if attrs[3] & ATTR_FILE_CLUMPSIZE:
805 | a = uint32_t.from_buffer(buf, offset)
806 | offset += sizeof(uint32_t)
807 | result.append(a.value)
808 | if attrs[3] & ATTR_FILE_DEVTYPE:
809 | a = uint32_t.from_buffer(buf, offset)
810 | offset += sizeof(uint32_t)
811 | result.append(a.value)
812 | if attrs[3] & ATTR_FILE_FILETYPE:
813 | a = uint32_t.from_buffer(buf, offset)
814 | offset += sizeof(uint32_t)
815 | result.append(a.value)
816 | if attrs[3] & ATTR_FILE_FORKCOUNT:
817 | a = uint32_t.from_buffer(buf, offset)
818 | offset += sizeof(uint32_t)
819 | result.append(a.value)
820 | if attrs[3] & ATTR_FILE_DATALENGTH:
821 | a = off_t.from_buffer(buf, offset)
822 | offset += sizeof(off_t)
823 | result.append(a.value)
824 | if attrs[3] & ATTR_FILE_DATAALLOCSIZE:
825 | a = off_t.from_buffer(buf, offset)
826 | offset += sizeof(off_t)
827 | result.append(a.value)
828 | if attrs[3] & ATTR_FILE_DATAEXTENTS:
829 | a = extentrecord.from_buffer(buf, offset)
830 | offset += sizeof(extentrecord)
831 | result.append(a.value)
832 | if attrs[3] & ATTR_FILE_RSRCLENGTH:
833 | a = off_t.from_buffer(buf, offset)
834 | offset += sizeof(off_t)
835 | result.append(a.value)
836 | if attrs[3] & ATTR_FILE_RSRCALLOCSIZE:
837 | a = off_t.from_buffer(buf, offset)
838 | offset += sizeof(off_t)
839 | result.append(a.value)
840 | if attrs[3] & ATTR_FILE_RSRCEXTENTS:
841 | a = extentrecord.from_buffer(buf, offset)
842 | offset += sizeof(extentrecord)
843 | result.append(a.value)
844 |
845 | # Fork attributes
846 | if attrs[4] & ATTR_FORK_TOTALSIZE:
847 | a = off_t.from_buffer(buf, offset)
848 | offset += sizeof(off_t)
849 | result.append(a.value)
850 | if attrs[4] & ATTR_FORK_ALLOCSIZE:
851 | a = off_t.from_buffer(buf, offset)
852 | offset += sizeof(off_t)
853 | result.append(a.value)
854 |
855 | # Extended common attributes
856 | if attrs[4] & ATTR_CMNEXT_RELPATH:
857 | a = attrreference_t.from_buffer(buf, offset)
858 | ofs = offset + a.attr_dataoffset
859 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
860 | offset += sizeof (attrreference_t)
861 | result.append(path)
862 | if attrs[4] & ATTR_CMNEXT_PRIVATESIZE:
863 | a = off_t.from_buffer(buf, offset)
864 | offset += sizeof(off_t)
865 | result.append(a.value)
866 | if attrs[4] & ATTR_CMNEXT_LINKID:
867 | a = uint64_t.from_buffer(buf, offset)
868 | offset += sizeof(uint64_t)
869 | result.append(a.value)
870 | if attrs[4] & ATTR_CMNEXT_NOFIRMLINKPATH:
871 | a = attrreference_t.from_buffer(buf, offset)
872 | ofs = offset + a.attr_dataoffset
873 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length])
874 | offset += sizeof (attrreference_t)
875 | result.append(path)
876 | if attrs[4] & ATTR_CMNEXT_REALDEVID:
877 | a = dev_t.from_buffer(buf, offset)
878 | offset += sizeof(dev_t)
879 | result.append(a.value)
880 | if attrs[4] & ATTR_CMNEXT_REALFSID:
881 | a = fsid_t.from_buffer(buf, offset)
882 | offset += sizeof(fsid_t)
883 | result.append(a.value)
884 | if attrs[4] & ATTR_CMNEXT_CLONEID:
885 | a = uint64_t.from_buffer(buf, offset)
886 | offset += sizeof(uint64_t)
887 | result.append(a.value)
888 | if attrs[4] & ATTR_CMNEXT_EXT_FLAGS:
889 | a = uint64_t.from_buffer(buf, offset)
890 | offset += sizeof(uint64_t)
891 | result.append(a.value)
892 |
893 | return result
894 |
895 | # Sadly, ctypes.get_errno() seems not to work
896 | __error = libc.__error
897 | __error.restype = POINTER(c_int)
898 |
899 | def _get_errno():
900 | return __error().contents.value
901 |
902 | def getattrlist(path, attrs, options):
903 | if not isinstance(path, bytes):
904 | path = path.encode('utf-8')
905 | attrs = list(attrs)
906 | if attrs[1]:
907 | attrs[1] |= ATTR_VOL_INFO
908 | alist = attrlist(bitmapcount=5,
909 | commonattr=attrs[0],
910 | volattr=attrs[1],
911 | dirattr=attrs[2],
912 | fileattr=attrs[3],
913 | forkattr=attrs[4])
914 |
915 | bufsize = _attrbuf_size(attrs)
916 | buf = create_string_buffer(bufsize)
917 |
918 | ret = _getattrlist(path, byref(alist), buf, bufsize,
919 | options | FSOPT_REPORT_FULLSIZE)
920 |
921 | if ret < 0:
922 | err = _get_errno()
923 | raise OSError(err, os.strerror(err), path)
924 |
925 | return _decode_attrlist_result(buf, attrs, options)
926 |
927 | def fgetattrlist(fd, attrs, options):
928 | if hasattr(fd, 'fileno'):
929 | fd = fd.fileno()
930 | attrs = list(attrs)
931 | if attrs[1]:
932 | attrs[1] |= ATTR_VOL_INFO
933 | alist = attrlist(bitmapcount=5,
934 | commonattr=attrs[0],
935 | volattr=attrs[1],
936 | dirattr=attrs[2],
937 | fileattr=attrs[3],
938 | forkattr=attrs[4])
939 |
940 | bufsize = _attrbuf_size(attrs)
941 | buf = create_string_buffer(bufsize)
942 |
943 | ret = _fgetattrlist(fd, byref(alist), buf, bufsize,
944 | options | FSOPT_REPORT_FULLSIZE)
945 |
946 | if ret < 0:
947 | err = _get_errno()
948 | raise OSError(err, os.strerror(err))
949 |
950 | return _decode_attrlist_result(buf, attrs, options)
951 |
952 | def statfs(path):
953 | if not isinstance(path, bytes):
954 | path = path.encode('utf-8')
955 | result = struct_statfs()
956 | ret = _statfs(path, byref(result))
957 | if ret < 0:
958 | err = _get_errno()
959 | raise OSError(err, os.strerror(err), path)
960 | return result
961 |
962 | def fstatfs(fd):
963 | if hasattr(fd, 'fileno'):
964 | fd = fd.fileno()
965 | result = struct_statfs()
966 | ret = _fstatfs(fd, byref(result))
967 | if ret < 0:
968 | err = _get_errno()
969 | raise OSError(err, os.strerror(err))
970 | return result
971 |
--------------------------------------------------------------------------------
/mac_alias/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import datetime
5 |
6 | ZERO = datetime.timedelta(0)
7 | class UTC (datetime.tzinfo):
8 | def utcoffset(self, dt):
9 | return ZERO
10 | def dst(self, dt):
11 | return ZERO
12 | def tzname(self, dt):
13 | return 'UTC'
14 |
15 | utc = UTC()
16 | mac_epoch = datetime.datetime(1904,1,1,0,0,0,0,utc)
17 | unix_epoch = datetime.datetime(1970,1,1,0,0,0,0,utc)
18 | osx_epoch = datetime.datetime(2001,1,1,0,0,0,0,utc)
19 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: UTF-8 -*-
3 |
4 | # ~/Library/Application Support/com.apple.sharedfilelist/
5 | # https://www.mac4n6.com/blog/2017/10/17/script-update-for-macmrupy-v13-new-1013-sfl2-mru-files
6 |
7 | import os
8 | import re
9 | import sys
10 | import json
11 | import time
12 | import pinyin
13 | import plistlib
14 | import ccl_bplist
15 | from mac_alias import Bookmark
16 | try:
17 | from urllib import unquote # Python 2.X
18 | except ImportError:
19 | from urllib.parse import unquote # Python 3+
20 |
21 |
22 | def BLOBParser_human(blob):
23 | # http://mac-alias.readthedocs.io/en/latest/bookmark_fmt.html
24 | try:
25 | b = Bookmark.from_bytes(blob)
26 | return "/" + u"/".join(b.get(0x1004, default=None))
27 | except Exception as e:
28 | print(e)
29 |
30 |
31 | # for 10.13
32 | def ParseSFL2(MRUFile):
33 | itemsLinkList = []
34 | try:
35 | with open(MRUFile, "rb") as plistfile:
36 | plist = ccl_bplist.load(plistfile)
37 | plist_objects = ccl_bplist.deserialise_NsKeyedArchiver(
38 | plist, parse_whole_structure=True)
39 | if plist_objects["root"]["NS.keys"][0] == "items":
40 | for item in plist_objects["root"]["NS.objects"][0]["NS.objects"]:
41 | attributes = dict(zip(item["NS.keys"], item["NS.objects"]))
42 | # print(attributes["Bookmark"].decode("iso-8859-1"))
43 | if "Bookmark" in attributes:
44 | if isinstance(attributes["Bookmark"], str):
45 | itemLink = BLOBParser_human(attributes["Bookmark"])
46 | elif isinstance(attributes["Bookmark"], bytes):
47 | itemLink = BLOBParser_human(attributes["Bookmark"])
48 | else:
49 | itemLink = BLOBParser_human(attributes["Bookmark"]['NS.data'])
50 | # print(itemLink)
51 | itemsLinkList.append(itemLink)
52 | return itemsLinkList
53 | except Exception as e:
54 | print("#ERROR:", e)
55 |
56 |
57 | # for 10.11, 10.12
58 | def ParseSFL(MRUFile):
59 | itemsLinkList = []
60 | try:
61 | with open(MRUFile, "rb") as plistfile:
62 | plist = ccl_bplist.load(plistfile)
63 | plist_objects = ccl_bplist.deserialise_NsKeyedArchiver(
64 | plist, parse_whole_structure=True)
65 | if plist_objects["root"]["NS.keys"][2] == "items":
66 | items = plist_objects["root"]["NS.objects"][2]["NS.objects"]
67 | for n, item in enumerate(items):
68 | # item["URL"]['NS.relative'] file:///xxx/xxx/xxx
69 | filePath = item["URL"]['NS.relative'][7:]
70 | # /xxx/xxx/xxx/ the last "/" make basename func not work
71 | if filePath[-1] == '/':
72 | filePath = filePath[:-1]
73 | itemsLinkList.append(filePath)
74 | return itemsLinkList
75 | except Exception as e:
76 | print(e)
77 |
78 |
79 | # for Finder
80 | def ParseFinderPlist(MRUFile):
81 | itemsLinkList = []
82 | try:
83 | with open(MRUFile, "rb") as plistfile:
84 | plist = ccl_bplist.load(plistfile)
85 | for item in plist["FXRecentFolders"]:
86 | if "file-bookmark" in item:
87 | blob = item["file-bookmark"]
88 | elif "file-data" in item:
89 | blob = item["file-data"]["_CFURLAliasData"]
90 | itemLink = BLOBParser_human(blob)
91 | # exclude / path
92 | if itemLink == "/":
93 | continue
94 | itemsLinkList.append(itemLink)
95 | return itemsLinkList
96 | except Exception as e:
97 | print(e)
98 |
99 |
100 | # for Sublime Text 3
101 | def ParseSublimeText3Session(sessionFile):
102 | with open(sys.argv[1]) as ff:
103 | jsonObj = json.load(ff)
104 | itemsLinkList = jsonObj["settings"]["new_window_settings"]["file_history"][0:15]
105 | return itemsLinkList
106 |
107 |
108 | # deprecated
109 | def ParseMSOffice2016Plist(MRUFile):
110 | itemsLinkList = []
111 | try:
112 | plistfile = plistlib.readPlist(MRUFile)
113 | for item in plistfile:
114 | itemsLinkList.append(unquote(item[7:]).decode('utf8'))
115 | return itemsLinkList
116 | except Exception as e:
117 | # When read binary property list
118 | # xml.parsers.expat.ExpatError: not well-formed (invalid token)
119 | print(e)
120 |
121 |
122 | # deprecated
123 | def ParseMSOffice2019Plist(MRUFile):
124 | itemsLinkList = []
125 | try:
126 | with open(MRUFile, "rb") as plistfile:
127 | plist = ccl_bplist.load(plistfile)
128 | itemsLinkList = [unquote(item[7:].encode("utf8")) for item in plist.keys()]
129 | return itemsLinkList
130 | except Exception as e:
131 | # When read xml property list
132 | # ccl_bplist.BplistError: Bad file header
133 | print(e)
134 |
135 |
136 | def ParseMSOfficePlist(MRUFile):
137 | itemsLinkList = []
138 | try:
139 | # office 2019
140 | with open(MRUFile, "rb") as plistfile:
141 | plist = ccl_bplist.load(plistfile)
142 | itemsLinkList = [unquote(item[7:].encode("utf8")).decode('utf8') for item in plist.keys()]
143 | return itemsLinkList
144 | except ccl_bplist.BplistError as e:
145 | # office 2016
146 | plistfile = plistlib.readPlist(MRUFile)
147 | for item in plistfile:
148 | itemsLinkList.append(unquote(item[7:]).decode('utf8'))
149 | return itemsLinkList
150 | except Exception as e:
151 | print(e)
152 |
153 |
154 | def checkFileList(fileList):
155 | newFileList = []
156 | excludedFolderList = os.environ["ExcludedFolders"].split(":") if os.environ["ExcludedFolders"] else []
157 | excludedFilesList = os.environ["ExcludedFiles"].split(":") if os.environ["ExcludedFiles"] else []
158 | for filePath in fileList:
159 | if not os.path.exists(filePath):
160 | continue
161 | fileExclude = False
162 | for exFilePath in excludedFilesList:
163 | exFilePath = os.path.expanduser(exFilePath)
164 | # 检测是否为文件路径
165 | if not os.path.isfile(exFilePath):
166 | break
167 | if os.path.samefile(filePath, exFilePath):
168 | fileExclude = True
169 | break
170 | for exFolderPath in excludedFolderList:
171 | exFolderPath = os.path.expanduser(exFolderPath)
172 | # 检测是否为文件夹路径
173 | if not os.path.isdir(exFolderPath):
174 | continue
175 | # change "/xxx/xx" to "/xxx/xx/"
176 | # for distinguish "/xxx/xx" and "/xxx/xx x/xx"
177 | exFolderPath = os.path.join(exFolderPath, "")
178 | # change "/xxx/xx" to "/xxx/xx/" for comparing exFolderPath "/xxx/xx/" and filePath "/xxx/xx"
179 | fileFullPath = (filePath + "/") if os.path.isdir(filePath) else filePath
180 | if fileFullPath.startswith(exFolderPath):
181 | fileExclude = True
182 | break
183 | if not fileExclude:
184 | newFileList.append(filePath)
185 | return newFileList
186 |
187 |
188 | # convert "abc你好def" to "abc ni hao def"
189 | def convert2Pinyin(filename):
190 | # convert "你好" to " ni hao "
191 | def c2p(matchObj):
192 | return " " + pinyin.get(matchObj.group(), format="strip", delimiter=" ") + " "
193 | # replace chinese character with pinyin
194 | return re.sub(u'[\u4e00-\u9fff]+', c2p, filename)
195 |
196 |
197 | if __name__ == '__main__':
198 | allItemsLinkList = []
199 | for filePath in sys.argv[1:]:
200 | if filePath.endswith(".sfl2"):
201 | if __debug__: print("#FileType: sfl2") # noqa
202 | itemsLinkList = ParseSFL2(filePath)
203 | if filePath.endswith(".sfl3"):
204 | if __debug__: print("#FileType: sfl3") # noqa
205 | itemsLinkList = ParseSFL2(filePath)
206 | elif filePath.endswith(".sfl"):
207 | if __debug__: print("#FileType: sfl") # noqa
208 | itemsLinkList = ParseSFL(filePath)
209 | elif filePath.endswith("com.apple.finder.plist"):
210 | if __debug__: print("#FileType: com.apple.finder.plist") # noqa
211 | itemsLinkList = ParseFinderPlist(filePath)
212 | elif filePath.endswith(".securebookmarks.plist"):
213 | if __debug__: print("#FileType: .securebookmarks.plist") # noqa
214 | itemsLinkList = ParseMSOfficePlist(filePath)
215 | elif filePath.endswith(".sublime_session"):
216 | if __debug__: print("#FileType: sublime_session") # noqa
217 | itemsLinkList = ParseSublimeText3Session(filePath)
218 | allItemsLinkList.extend(itemsLinkList)
219 | allItemsLinkList = checkFileList(allItemsLinkList)
220 |
221 | # use current app to open recent documents of current app
222 | currentAppPath = os.getenv("currentAppPath")
223 | if currentAppPath:
224 | result = {"variables": {"currentAppPath": currentAppPath}, "items": []}
225 | else:
226 | result = {"items": []}
227 |
228 | for n, item in enumerate(allItemsLinkList):
229 | # remove records of file not exist
230 | if not os.path.exists(item):
231 | continue
232 | modifiedTimeSecNum = os.path.getmtime(item)
233 | modifiedTime = time.strftime("%m-%d %H:%M", time.localtime(modifiedTimeSecNum))
234 | filename = os.path.basename(item)
235 | temp = {
236 | "type": "file",
237 | "title": filename,
238 | "autocomplete": filename,
239 | # replace "." with space for searching filename extension
240 | "match": filename.replace('.', ' ') + " " + convert2Pinyin(filename),
241 | "icon": {"type": "fileicon", "path": item},
242 | "subtitle": u"❖" + modifiedTime + u" ❥" + item.replace(os.environ["HOME"], "~"),
243 | "arg": item
244 | }
245 | result['items'].append(temp)
246 | if result['items']:
247 | print(json.dumps(result))
248 | else:
249 | # when result list is empty
250 | print('{"items": [{"title": "None Recent Record","subtitle": "(*´・д・)?"}]}')
251 |
--------------------------------------------------------------------------------
/pinyin/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .pinyin import *
4 |
--------------------------------------------------------------------------------
/pinyin/_compat.py:
--------------------------------------------------------------------------------
1 | import sys
2 | py2 = sys.version_info < (3, 0)
3 |
4 |
5 | if py2:
6 | str_type = unicode
7 |
8 | def u(s):
9 | if not isinstance(s, unicode):
10 | s = unicode(s, "utf-8")
11 | return s
12 |
13 | else:
14 | str_type = str
15 |
16 | def u(s):
17 | return s
18 |
--------------------------------------------------------------------------------
/pinyin/cmd.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from pinyin import get
3 |
4 | from pinyin._compat import u
5 |
6 |
7 | def pinyin():
8 | parser = argparse.ArgumentParser()
9 | parser.add_argument("chars", help="Input chinese words")
10 | args = parser.parse_args()
11 |
12 | if not args.chars:
13 | parser.print_help()
14 | return
15 |
16 | print(get(u(args.chars)))
17 |
--------------------------------------------------------------------------------
/pinyin/pinyin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import itertools
5 | import unicodedata
6 |
7 | from ._compat import u
8 |
9 | __all__ = ['get', 'get_pinyin', 'get_initial']
10 |
11 |
12 | # init pinyin dict
13 | pinyin_dict = {}
14 | pinyin_tone = {}
15 | dat = os.path.join(os.path.dirname(__file__), "Mandarin.dat")
16 | with open(dat) as f:
17 | for line in f:
18 | k, v = line.strip().split('\t')
19 | pinyin_dict[k] = u(v.lower().split(" ")[0][:-1])
20 | pinyin_tone[k] = int(v.lower().split(" ")[0][-1])
21 |
22 |
23 | def _pinyin_generator(chars, format):
24 | """Generate pinyin for chars, if char is not chinese character,
25 | itself will be returned.
26 | Chars must be unicode list.
27 | """
28 | for char in chars:
29 | key = "%X" % ord(char)
30 | pinyin = pinyin_dict.get(key, char)
31 | tone = pinyin_tone.get(key, 0)
32 |
33 | if tone == 0 or format == "strip":
34 | pass
35 | elif format == "numerical":
36 | pinyin += str(tone)
37 | elif format == "diacritical":
38 | # Find first vowel -- where we should put the diacritical mark
39 | vowels = itertools.chain((c for c in pinyin if c in "aeo"),
40 | (c for c in pinyin if c in "iuv"))
41 | vowel = pinyin.index(next(vowels)) + 1
42 | pinyin = pinyin[:vowel] + tonemarks[tone] + pinyin[vowel:]
43 | else:
44 | error = "Format must be one of: numerical/diacritical/strip"
45 | raise ValueError(error)
46 |
47 | yield unicodedata.normalize('NFC', pinyin)
48 |
49 |
50 | def get(s, delimiter='', format="diacritical"):
51 | """Return pinyin of string, the string must be unicode
52 | """
53 | return delimiter.join(_pinyin_generator(u(s), format=format))
54 |
55 |
56 | def get_pinyin(s):
57 | """This function is only for backward compatibility, use `get` instead.
58 | """
59 | import warnings
60 | warnings.warn('Deprecated, use `get` instead.')
61 | return get(s)
62 |
63 |
64 | def get_initial(s, delimiter=' '):
65 | """Return the 1st char of pinyin of string, the string must be unicode
66 | """
67 | initials = (p[0] for p in _pinyin_generator(u(s), format="strip"))
68 | return delimiter.join(initials)
69 |
70 | tonemarks = ["", u("̄"), u("́"), u("̌"), u("̀"), ""]
71 |
--------------------------------------------------------------------------------