资讯动态

Tinycast 迁移指南:Raycast `.rayconfig` 文件格式解析、AES-256-GCM 解密与数据导入原理

发布时间:2026/9/19 11:24:46 来源:尧图企业网站定制
Tinycast 迁移指南Raycast.rayconfig文件格式解析、AES-256-GCM 解密与数据导入原理【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast本文围绕 Tinycast 的 Raycast 数据迁移功能展开深入解析.rayconfig导出文件的 RAYCFG3 容器格式、scrypt 密钥派生与 AES-256-GCM 解密链路并完整说明设置、热键、剪贴板历史、Snippets 与 Quicklinks 等 11 类数据到 Tinycast 域模型的映射规则。读完本文你将掌握该导入功能支持的格式范围、底层解密与校验流程、每个类别的字段映射细节以及对应的测试验证方式可直接对照仓库源码进行二次开发或排障。一、支持范围只认 v2.x 的.rayconfigTinycast 的 Raycast 导入功能只读取 Raycastv2.x导出的.rayconfig文件。该文件是一个RAYCFG3容器——即带版本号魔数的压缩 加密容器内部承载一份 AES-256-GCM 加密载荷密钥由 scrypt 从口令派生。需要特别注意的是v1.x 的导出格式与 Raycast X beta 格式都已不再支持。从v0.10.5起这两类旧格式不是保留兼容而是直接删除、不再承载。这意味着如果你手头只有老版本 Raycast 的导出文件需要先在 Raycast 中重新导出 v2.x 格式再交给 Tinycast。该判断有明确的源码依据解码器 RaycastDecoder.swift 中硬编码了容器版本常量containerSchemaVersion 3任何其他schemaVersion一律抛出.corrupt错误拒绝读取。二、容器格式RAYCFG3 的字节级结构原文档给出了完整的线格式wire format这是理解整个导入链路的地基file RAYCFG3\n ‖ UInt32LE(header.count) ‖ gzip(header JSON) ‖ ciphertext ‖ tag(16) body AES-256-GCM(gzip(payload JSON)) key scrypt(passphrase, salt, N16384, r8, p1, dkLen32)对照 RaycastDecoder.swift 的解析逻辑容器各部分的作用如下段长度/编码说明魔数8 字节字面量RAYCFG3\n含尾部换行用于文件识别header 长度4 字节小端 UInt32位于文件偏移 8..12指明 gzip 后的 header JSON 长度header变长gzip 压缩JSON含schemaVersion、encryption.iv、encryption.salt均为 16 字节的 hex 字符串ciphertext变长加密后的载荷密文tag16 字节AES-GCM 认证标签头部长度字段本身不受认证保护因此解码器对其做了严格边界检查fixedHeaderLength 12魔数 8 字节 长度 4 字节header 长度必须 0且 maximumHeaderLength1 MB并且12 headerLength不能超出文件总长度否则直接判定为损坏.corrupt不会进入任何解密步骤。密钥派生零依赖的 scrypt 实现解密密钥通过 Scrypt.derive 派生参数固定为N16384, r8, p1, dkLen32RFC 7914 标准参数。值得注意的是该实现是仓库自带的完整 scrypt 实现不依赖任何第三方加密库它由 PBKDF2-HMAC-SHA256、ROMix、BlockMix 与 Salsa20/8 四部分拼装而成全程只使用 CryptoKit 的 HMAC 原语。这意味着导入功能在编译和分发时零额外依赖也便于在无优化构建的测试环境里精确控制其性能开销。解密与解压的完整链路一次成功的解密在 RaycastDecoder.decrypt 中按以下顺序执行校验文件以RAYCFG3\n开头否则抛.notRaycastFile读取并校验 header 长度解压 header JSON解码出schemaVersion、iv、salt校验schemaVersion 3校验iv与salt各为 16 字节用scrypt(passphrase, salt, N16384, r8, p1, dkLen32)派生 32 字节密钥以iv作为 nonce构造AES.GCM.SealedBox并AES.GCM.open认证失败口令错误抛.incorrectPassphrase对解密得到的载荷 gzip 解压得到最终 payload JSON。解压上限内存边界而非 zip 炸弹防护解码器对解压输出设置了两个上限理解它们的语义差异很重要header 解压上限 1 MBheader 是未经认证的数据因此这个上限同时也是对不可信输入的第一道防线payload 解压上限 512 MB由于 AES-GCM 已经对密文完成了认证这一上限不是zip 炸弹防护而纯粹是内存边界。文档明确指出剪贴板内容繁多的导出解压后可能超过 64 MB因此 payload 上限定为 512 MBmaximumPayloadLength 512 * 1024 * 1024见 RaycastDecoder.swift。如果解压超限会抛出.tooLarge错误对应错误文案建议清理部分 Raycast 剪贴板历史后重新导出见 RaycastImportError.swift。口令从哪里来Tinycast 从不读取钥匙串一个容易被忽略的事实是即使你在 Raycast 中从未设置过密码导出文件也总是加密的——Raycast 会自行生成一个口令并存入登录钥匙串服务Raycast、账户export_passphrase可在 Raycast → Settings → Extensions → Export Settings Data 中查看。Tinycast不会读取钥匙串口令完全由用户在导入界面手动输入。这既避免了权限膨胀也保证了导入动作的完全可审计。三、加载即识别容器签名驱动的文件判定导入界面有一个贴心设计识别一个文件是否是 Raycast 导出只看容器签名魔数不需要口令。这正是 RaycastDecoder.isExport 的职责——它只检查原始字节是否以RAYCFG3\n开头。因此 Backup 面板在用户选定文件的瞬间就会运行该判定对应 BackupActions.isRaycastExport以.mappedIfSafe方式只读文件前部字节如果用户随后输入了错误口令界面报的是口令错误而不会误报这不是 Raycast 导出文件。错误语义的分层非常清晰RaycastImportError.swift错误触发条件notRaycastFile魔数不匹配incorrectPassphraseAES-GCM 认证失败或文件被篡改corrupt结构非法头部截断、schema 版本不符、iv/salt 非法、header 长度越界等tooLarge解压输出超过 512 MB 上限四、数据映射Raycast 字段如何落到 Tinycast 域模型解密得到的 payload 是按类别组织的 JSON顶层包含settings、clipboardHistory、snippets顶层对象其条目以title命名、quicklinks内含quicklinks与openWithPlatforms。把这些 Raycast 原生值翻译成 Tinycast 域类型的工作由 RaycastImportReader.swift 完成RaycastImport.Result汇总为四块数据加一个统计值RaycastImport.swiftbackup: SettingsBackup设置、热键、收藏、别名clipboard: [ClipboardItem]剪贴板历史snippets: [Snippet]quicklinks: [Quicklink]missingImages: Int文件已不存在的图片剪贴记录数下面逐类展开映射细节。4.1 设置项映射mapSettings 读取settings.general下的字段Raycast 字段Tinycast 字段映射说明openAtLoginlaunchAtLogin布尔直传hyperKeyIncludeShifthyperKeyIncludesShift布尔直传hyperKeyCodehyperKey字符串代码经hyperKeyCodes表转换caps_lock/right_control/right_shift/right_option/right_command没有映射到.none的项因此不会清空已有设置showInMenuBarshowInMenuBar布尔直传skinToneemojiSkinTone通过递归搜索skinTone键获取default映射为.nonepopToRootTimeoutpopToRootSeconds仅精确匹配不在 Tinycast 选项集内的超时值直接跳过不做截断windowModecompactModeRaycast 是字符串Tinycast 只有紧凑开关compact→ trueshowFavoritesInCompactModeshowFavoritesInCompactMode布尔直传4.2 热键映射一律映射为.comboRaycast 的每个热键都遵循同一形态因此 binding(from:) 统一解析按键必须是LayoutIndependent键码type LayoutIndependent保留code作为 Carbon 键码修饰键名称映射Meta→Command、Ctrl→Control、Alt→Option、Shift→ShiftRaycast 没有双击double-tap绑定所以导入的热键一律构造为.combo类型。热键来源有三处mapHotkeyssettings.general.globalHotkey→ 调色板开关热键settings.commands[]中的macosHotkey按extensionId分发e:r:clipboard-history→ Tinycast 剪贴板历史命令e:r:emoji-picker→ Tinycast 表情搜索命令e:r:applications→ 应用热键按 bundle ID 归类见下文路径解析hyperKey相关设置随.shortcuts选项一并导入。4.3 应用路径 → Bundle ID 的解析Raycast 的应用命令 ID 中启动的应用路径藏在::::分隔符之后。appPath(fromCommandID:)取出路径尾部再通过Bundle(url:)?.bundleIdentifier解析为 bundle IDRaycastImportReader.swift。热键、收藏favorites和别名aliases三个映射器共用这一套解析逻辑且只有e:r:applications扩展的命令才参与映射。收藏读取favoriteOrder并按升序排序保留 Raycast 中的顺序输出为 bundle ID 数组mapFavorites别名读取非空、去除首尾空白的alias以 bundle ID 为键写入字典mapAliases。4.4 剪贴板历史映射mapClipboard 处理clipboardHistory.clipboardEntries每条记录的items[].representations[]是嵌套结构需扁平化遍历时间戳可能带小数秒优先用ISO8601DateFormatter的.withFractionalSeconds解析失败再退回整秒格式parseDate文本取第一个mimeType以text/plain开头的 representation 的content非空则生成文本剪贴条目图片取mimeType以image/开头且contentType url的 representation其content是文件路径。只有当文件仍然存在时才生成图片剪贴条目文件不存在的记录计入missing计数——是上报而不是静默丢弃导入结果中missingImages会展示给用户见 BackupActions.raycastText。每条导入记录都会获得全新的UUIDRaycast 原有的 ULID 被丢弃——这与 JSON quicklink 导入的行为一致。4.5 Snippets 映射RaycastSnippetImport.parse 读取顶层snippets对象的snippets数组每条以title为名称、text为内容keyword去除首尾空白后非空才保留。注意这里snippets是顶层对象不是数组其内部的snippets键才是条目数组。4.6 Quicklinks 映射合并而非覆盖RaycastQuicklinkImport.parse 读取quicklinks对象包含quicklinks数组与openWithPlatforms平台映射{Query}占位符会被重写为 Tinycast 的{argument}rewrittenLink重写是令牌级的只有命令字为query忽略大小写的{...}令牌才被改写其余原样保留openWith若以/开头视为应用路径否则视为openWithPlatforms中的平台 id两者最终都解析为 bundle ID——与应用热键的解析方式相同createdAt同样支持小数秒与整秒两种时间戳。导入时 quicklinks 走的是 QuicklinkArchive.merge 语义向现有库中新增绝不整体替换。并且只要成功导入至少一个 quicklink就会打开quicklinksEnabled开关——因为打开一个链接不授予任何权限类见 BackupActions.importRaycast。4.7 脚本命令不在.rayconfig中需要注意边界Raycast 的脚本命令script commands不在.rayconfig文件里它们是 Raycast 指向的文件夹中的文件因此有独立的导入器详见 custom-commands.md 中Importing Raycast scripts一节。五、分类导入11 个独立可选的类别用户不必全量导入。RaycastImportOptionsRaycastImport.swift是位掩码 OptionSet定义 11 个独立类别 all组合位类别说明10shortcuts热键 相关设置含 hyper key11favorites收藏应用12emojiSkinTone表情肤色13launchAtLogin登录自启14menuBarVisibility菜单栏图标显示15clipboardHistory剪贴板历史含每应用排除列表16popToRoot返回根的超时设置17compactMode紧凑模式及其中收藏显示18snippets文本片段19aliases应用别名110quicklinks快捷链接Result.selecting(_:)RaycastImport.swift按所选类别裁剪结果由于apply()本身是逐字段的裁剪只需丢弃未选类别对应的字段即可不会影响已选部分。界面上的类别选择器RaycastImportSelection.swift以三列网格 全选/全不选按钮呈现与 Backup 面板和首次启动引导共用。六、导入执行流程主线程外完成重活BackupActions.importRaycast 是导入的入口其执行顺序与容错策略如下解密与映射移出主线程Task.detached(priority: .userInitiated)autoreleasepool包裹RaycastImportReader.read(...).selecting(options)使大型 JSON 树能一次性释放内存Snippets 导入是上报而非抛出importSnippets失败只记录snippetsError不影响其余数据继续导入导入前若 snippets 开关已开会先启动 snippets 存储让导入的片段立刻进入启动器若导入成功但开关未开snippetsNeedEnabling会提示用户去设置中开启关键词导入任何内容都不会授予按键监听权限Quicklinks 合并走addImportedQuicklinks若 quicklinks 存储不可用记quicklinksError但继续设置与热键应用result.backup.apply(to: core)返回逐类别的ApplySummary剪贴板历史写入clipboardStore.importEntries返回RaycastOutcome包含各项导入数量、缺失图片数与错误提示由 raycastText 汇总为一句句可读的摘要并提示退出并重开 Tinycast 以完成导入。此外导入前后还有两个贴心动作pickRaycastFile()提供共用的.rayconfig文件选择器Backup 面板与引导共用quitRaycast()会退出正在运行的 Raycast凡 bundle ID 以com.raycast为前缀、且不是后台无界面进程的应用避免导入后热键冲突BackupActions.swift。七、工程架构纯层 / 平台层分离导入链路刻意做了架构分层与 WindowManagement 使用的模式一致RaycastDecoder是纯解码层只做容器解包与解密返回 Raycast 自身的载荷字节完全不接触 AppKit/UI。因此它能被 raycast-test.swift 在无 UI 的独立 harness 中编译运行RaycastImportReader是平台层需要 AppKitBundle解析 bundle ID、文件系统检查因此位于Service/目录由应用构建覆盖而非测试 harnessRaycastImport只是数据Result、selecting(_:)与RaycastImportOptions。由谁校验载荷也因此被确定下来解码器只保证容器本身有效RaycastImportReader才负责把载荷映射为域模型——映射时的精确校验如PopToRootTimeout、EmojiSkinTone、HyperKeyPhysicalKey、KeyShortcut等类型约束都发生在 reader 层解码器保持类型无关。八、测试与验证在 harness 中构建自己的夹具raycast-test.swift 完整覆盖了容器识别、解密、gzip 切片与解压上限四条路径其设计理念与文档的 Invariants 严格对应绝不提交真实.rayconfig作为夹具测试在进程内用Scrypt.deriveAES.GCM.seal自行构建容器口令12345678、固定 salt/iv既避免把用户真实数据引入仓库也让字节完全可控scrypt 只派生一次fixture用一次 key derivation 生成全部测试数据因为无优化构建下每次 scrypt 派生要花费数秒识别测试isExport对空数据、无签名数据、RAYCFG3缺换行都返回 false验证签名包含尾部换行解密测试正确口令可还原明文错误口令抛.incorrectPassphrase非零起始索引的Data切片slice同样能正确解密验证解码器内部偏移量从切片自身起点计算前置拒绝测试去掉签名、短于固定头、未来 schema 版本schemaVersion: 4都在触发 key derivation 之前被拒绝对0..payloadStart的每个截断位置逐一验证要么.notRaycastFile截断在魔数内要么.corrupt对 header 长度字段注入0、1、0x00100001、0xffffffff等越界值确认全部被拒gzip 上限测试70 MB 载荷在默认 64 MB 上限下抛tooLarge在 512 MB payload 上限下可正常解压——这个夹具同样在运行时构建因为超过默认上限的夹具无法常驻仓库。九、小结Tinycast 的 Raycast 导入是一个把外部加密格式 → 内部域模型做得相当严谨的功能容器签名先行识别、错误语义精确分层、scrypt 参数与 AES-256-GCM 与 Raycast v2.x 完全对齐、11 类数据各自拥有明确的字段映射、解压上限按已认证/未认证区分设计并通过纯层/平台层分离让核心解码逻辑可以在无 UI 的测试 harness 中被穷举验证。如果你想在迁移后继续导入 Raycast 脚本命令可查阅 custom-commands.md 中的 Raycast 脚本导入章节备份与恢复的整体设计则见 backup.md。【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价