ElyPDF: an OS permission error shows as an error, never a missing text key (P-01); component library f834f55
This commit is contained in:
17 files changed
+90
-25
No files matched your search
Generated
+1
-1
@@ -1964,7 +1964,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ely-gpui-component"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=b0dbfd429c17819f6c143fd365852244d497514a#b0dbfd429c17819f6c143fd365852244d497514a"
|
||||
source = "git+https://github.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=f834f55c16b38d86fbc243b2d7719667503f4b01#f834f55c16b38d86fbc243b2d7719667503f4b01"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ encoding_rs = "0.8"
|
||||
jiff = "0.2"
|
||||
anyhow = "1"
|
||||
directories = "6"
|
||||
ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "b0dbfd429c17819f6c143fd365852244d497514a" }
|
||||
ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "f834f55c16b38d86fbc243b2d7719667503f4b01" }
|
||||
env_logger = "0.11"
|
||||
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
|
||||
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
|
||||
|
||||
@@ -39,7 +39,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木
|
||||
|
||||
## 组件映射
|
||||
|
||||
依赖 `ely-gpui-component`(pin b0dbfd4,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
|
||||
依赖 `ely-gpui-component`(pin f834f55,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
|
||||
|
||||
| 需要 | 用 |
|
||||
| --- | --- |
|
||||
@@ -184,7 +184,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
|
||||
|
||||
## 决策
|
||||
|
||||
- 2026-10-01:上游版本以 `scripts/fetch-references.sh` 为准。LibreOffice 26.2.6(8221e31)、MuPDF 1.28.5(8ad45e9)、gpui 为 `gpui-pre` =0.3.8 与 `gpui-pre-platform` =0.3.8(crates.io,2026-10-07 起,随组件库 K01)、组件库 b0dbfd4(2026-10-09 起)。
|
||||
- 2026-10-01:上游版本以 `scripts/fetch-references.sh` 为准。LibreOffice 26.2.6(8221e31)、MuPDF 1.28.5(8ad45e9)、gpui 为 `gpui-pre` =0.3.8 与 `gpui-pre-platform` =0.3.8(crates.io,2026-10-07 起,随组件库 K01)、组件库 f834f55(2026-10-09 起)。
|
||||
- 2026-10-02:组件库 7380afb 起:对话框与面板的 Enter 提交改走字段的 key-down 动作(macOS IME 天然守卫,不再提前提交);面板族文案经组件 `i18n::text`(内建 en/zh-CN,应用目录可覆盖);核心控件带 role/aria;对话框带角色、名称与说明,Cancel/Close 与禁用说明本地化。同仓库 `~/Documents/GitHub/Ely-GPUI-Components` 修复并经 SSH 推送(本机 HTTPS keychain 令牌失效)。2026-10-04 组件库 b6cb94c:`NumberInput::on_commit` 只在 Enter、失焦、步进、拖动时触发,键入不触发;改文档的数值框用它,否则每个键入数字都成一条内核命令与一步撤销。同批带入上游的 Checkbox、Switch、Select 等 role/aria 与 RenderSurface。c0f17ee:`ToggleButton::disabled`(不可按、无 Tab 停靠、说明不可用,提示可写原因)。967d1e4:`ContextMenu::manual`(按属主编号的请求在某点打开,右键不开),供行在按下之后才到的菜单,如内核给的拼写建议。
|
||||
- 2026-10-06:审查者为探针复制的源码树须用自己的 `CARGO_TARGET_DIR`(`scripts/review.sh` 写明);写完结论前删掉这些副本与其构建目录(曾积到 /tmp 近 900 GB,磁盘写满、链接失败):曾编进本仓库 `target/`,ely-lok 里烘焙的源码路径指向已删的副本,单测找不到 worker。
|
||||
- 2026-10-03:每条任务由 `scripts/review.sh` 在 herdr 分栏里交给 codex(gpt-6.1-sol,effort high;本机 `codex` 别名已带 `--yolo`,重复传参会报错)审查,最多三轮。此前各轮用 gpt-6-astra xhigh。
|
||||
|
||||
@@ -136,7 +136,7 @@ impl Viewer {
|
||||
}
|
||||
|
||||
fn failed_write(&self, name: &str, error: &io::Error, cx: &mut App) {
|
||||
if error.kind() == io::ErrorKind::PermissionDenied {
|
||||
if ely_pdf_engine::refused(error).is_some() {
|
||||
return self.failed(error, cx);
|
||||
}
|
||||
let text = Self::text(cx, "office.error.save", &[("name", name), ("error", &error.to_string())]);
|
||||
|
||||
@@ -46,9 +46,9 @@ fn masked(window: &mut Window, cx: &mut Context<Viewer>) -> Entity<TextInput> {
|
||||
impl Viewer {
|
||||
/// A refusal names what is missing.
|
||||
pub(crate) fn failed(&self, error: &io::Error, cx: &mut App) {
|
||||
match error.kind() {
|
||||
io::ErrorKind::PermissionDenied => self.toast(crate::view::restricted(&error.to_string(), cx), Severity::Warning, cx),
|
||||
_ => self.panel_failed(&error.to_string(), cx),
|
||||
match ely_pdf_engine::refused(error) {
|
||||
Some(needed) => self.toast(crate::view::restricted(needed, cx), Severity::Warning, cx),
|
||||
None => self.panel_failed(&error.to_string(), cx),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,39 @@ fn pages_export_as_images_and_text() {
|
||||
assert!(text.contains("First") && text.contains("Second"), "{text}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_write_the_system_refuses_reports_it_without_a_crash() {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let path = file(dir.path(), "doc.pdf", &[(612.0, 792.0, &["First"])]);
|
||||
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
|
||||
shown(cx, &viewer);
|
||||
let locked = dir.path().join("locked");
|
||||
std::fs::create_dir(&locked).expect("a folder");
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o500)).expect("read-only");
|
||||
let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id);
|
||||
let txt = locked.join("doc.txt");
|
||||
let how = Optimizing { compress: true, from: 300, to: 0 };
|
||||
// The window's toasts live on its root.
|
||||
let root = cx.update(|window, app| {
|
||||
let root = ely_ui::document_root(ely_platform::AppId::Pdf, viewer.clone().into(), &ely_ui::commands::FILE, Vec::new(), window, app);
|
||||
viewer.update(app, |viewer, _| viewer.core.root = root.downgrade());
|
||||
root
|
||||
});
|
||||
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.write_export(id, Kind::Text, how, txt.clone(), cx)));
|
||||
let failed = |app: &gpui::App| {
|
||||
let toasts = root.read(app).toasts(app);
|
||||
toasts.iter().any(|(text, severity)| {
|
||||
*severity == Some(ely_gpui_component::primitives::Severity::Danger) && text.contains("Permission denied")
|
||||
})
|
||||
};
|
||||
settle(cx, failed);
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700)).expect("writable again");
|
||||
assert!(!txt.exists(), "nothing is written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_file_opens_repaired_and_saves_as_a_new_file() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
|
||||
@@ -227,10 +227,10 @@ impl PdfView {
|
||||
pub fn fail(&self, what: &str, error: &io::Error, cx: &mut Context<Self>) {
|
||||
// Engine text may quote the document: the user sees it.
|
||||
log::error!("pdf: {what} failed ({:?})", error.kind());
|
||||
let text = match error.kind() {
|
||||
let text = match ely_pdf_engine::refused(error) {
|
||||
_ if ely_pdf_engine::is_stale(error) => ely_ui::i18n::text(cx, "pdf.object.changed", &[]),
|
||||
io::ErrorKind::PermissionDenied => restricted(&error.to_string(), cx),
|
||||
_ => ely_ui::i18n::text(cx, "pdf.failed", &[("what", what), ("error", &error.to_string())]),
|
||||
Some(needed) => restricted(needed, cx),
|
||||
None => ely_ui::i18n::text(cx, "pdf.failed", &[("what", what), ("error", &error.to_string())]),
|
||||
};
|
||||
cx.emit(Failed(text.to_string()));
|
||||
}
|
||||
|
||||
@@ -293,10 +293,30 @@ impl Drop for Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// A permission refusal is `PermissionDenied`, named by what it needs.
|
||||
/// A request the document's permissions refused, by what it needs.
|
||||
#[derive(Debug)]
|
||||
pub struct Refused(pub &'static str);
|
||||
|
||||
impl std::fmt::Display for Refused {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", crate::proto::may::REFUSED, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Refused {}
|
||||
|
||||
/// What a permission refusal needs; none for other errors.
|
||||
pub fn refused(error: &io::Error) -> Option<&'static str> {
|
||||
error.get_ref()?.downcast_ref::<Refused>().map(|refused| refused.0)
|
||||
}
|
||||
|
||||
/// A worker error; a refusal names a permission it knows.
|
||||
fn refusal(text: String) -> io::Error {
|
||||
match text.strip_prefix(crate::proto::may::REFUSED) {
|
||||
Some(needed) => io::Error::new(io::ErrorKind::PermissionDenied, needed.to_string()),
|
||||
let named = text
|
||||
.strip_prefix(crate::proto::may::REFUSED)
|
||||
.and_then(|needed| crate::proto::may::NAMES.iter().find(|(_, name)| *name == needed));
|
||||
match named {
|
||||
Some((_, name)) => io::Error::other(Refused(name)),
|
||||
None => io::Error::other(text),
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ pub mod proto;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod sample;
|
||||
|
||||
pub use client::{Doc, Opened, Tile, Worker, is_stale};
|
||||
pub use client::{Doc, Opened, Refused, Tile, Worker, is_stale, refused};
|
||||
pub use content::{Drawn, TextBlock, Writing, Wrote};
|
||||
pub use ocr::{OcrLine, tessdata};
|
||||
pub use output::Optimizing;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Images, text, optimized copies, printing leave, repaired files (C11.8).
|
||||
|
||||
use std::{io::ErrorKind, path::Path};
|
||||
use std::path::Path;
|
||||
|
||||
use ely_pdf_engine::{
|
||||
Doc, Lock, Optimizing, Worker,
|
||||
@@ -89,7 +89,8 @@ fn printing_and_copying_need_leave() {
|
||||
worker.protect(doc, &limited, Some(&Lock { user: String::new(), owner: "owner".into(), allow: may::EDIT })).expect("protects");
|
||||
let (mut other, read) = opened(&limited);
|
||||
for refused in [other.print_copy(read, &dir.path().join("p.pdf")), other.export_text(read, &dir.path().join("t.txt"))] {
|
||||
assert_eq!(refused.expect_err("refused").kind(), ErrorKind::PermissionDenied);
|
||||
let refused = refused.expect_err("refused");
|
||||
assert!(ely_pdf_engine::refused(&refused).is_some(), "refused: {refused}");
|
||||
}
|
||||
worker.print_copy(doc, &dir.path().join("print.pdf")).expect("an unprotected file prints");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Protected copies and what they allow (PDF-11).
|
||||
|
||||
use std::{io::ErrorKind, path::Path};
|
||||
use std::path::Path;
|
||||
|
||||
use ely_pdf_engine::{
|
||||
Doc, Lock, Worker,
|
||||
@@ -43,7 +43,7 @@ fn a_protected_copy_asks_and_holds_its_limits() {
|
||||
let allowed = worker.permissions(opened.doc).expect("reads");
|
||||
assert_eq!(allowed & (may::PRINT | may::COPY | may::EDIT | may::OWNER), may::PRINT);
|
||||
let refused = worker.rotate_pages(opened.doc, &[0], 90).expect_err("refused");
|
||||
assert_eq!((refused.kind(), refused.to_string()), (ErrorKind::PermissionDenied, "assemble".to_string()));
|
||||
assert_eq!(ely_pdf_engine::refused(&refused), Some("assemble"));
|
||||
let text: String = worker.text(opened.doc, 0).expect("still reads").iter().map(|c| c.c).collect();
|
||||
assert!(text.contains("Hello"));
|
||||
}
|
||||
@@ -72,7 +72,7 @@ fn changing_protection_needs_the_owner() {
|
||||
worker.unlock(doc, "user").expect("unlocks");
|
||||
let bare = dir.path().join("bare.pdf");
|
||||
let refused = worker.protect(doc, &bare, None).expect_err("refused");
|
||||
assert_eq!(refused.kind(), ErrorKind::PermissionDenied);
|
||||
assert!(ely_pdf_engine::refused(&refused).is_some(), "refused: {refused}");
|
||||
assert!(!bare.exists());
|
||||
worker.owner(doc, "owner").expect("tries");
|
||||
worker.protect(doc, &bare, None).expect("unprotects");
|
||||
@@ -103,7 +103,7 @@ fn a_file_with_no_open_password_still_holds_limits() {
|
||||
let opened = other.open(&out).expect("opens");
|
||||
assert!(!opened.locked);
|
||||
let refused = other.annotate(opened.doc, &[]).expect_err("refused");
|
||||
assert_eq!((refused.kind(), refused.to_string()), (ErrorKind::PermissionDenied, "annotate".to_string()));
|
||||
assert_eq!(ely_pdf_engine::refused(&refused), Some("annotate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Signing by update and checking what was signed (PDF-12, QA-15).
|
||||
|
||||
use std::{io::ErrorKind, path::{Path, PathBuf}};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ely_pdf_engine::{
|
||||
Digest, Doc, Lock, Signing, Worker,
|
||||
@@ -128,7 +128,7 @@ fn a_new_field_needs_leave_to_change_the_page() {
|
||||
worker.protect(doc, &limited, Some(&lock)).expect("protects");
|
||||
let (mut other, read) = opened(&limited);
|
||||
let refused = other.sign(read, &dir.path().join("signed.pdf"), &signing(None, "S", "ely")).expect_err("refused");
|
||||
assert_eq!(refused.kind(), ErrorKind::PermissionDenied);
|
||||
assert!(ely_pdf_engine::refused(&refused).is_some(), "refused: {refused}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -154,6 +154,11 @@ impl Root {
|
||||
});
|
||||
}
|
||||
|
||||
/// The toasts on show: title and severity, oldest first.
|
||||
pub fn toasts(&self, cx: &gpui::App) -> Vec<(SharedString, Option<Severity>)> {
|
||||
self.toaster.read(cx).shown()
|
||||
}
|
||||
|
||||
/// A toast whose button opens the output `path` (§16).
|
||||
pub fn toast_output(
|
||||
&mut self,
|
||||
|
||||
@@ -33,7 +33,7 @@ fetch() { # name url rev [sparse paths...]
|
||||
echo "fetch $name $rev"
|
||||
}
|
||||
|
||||
fetch Ely-GPUI-Components https://github.com/ZacharyZhang-NY/Ely-GPUI-Components.git b0dbfd429c17819f6c143fd365852244d497514a
|
||||
fetch Ely-GPUI-Components https://github.com/ZacharyZhang-NY/Ely-GPUI-Components.git f834f55c16b38d86fbc243b2d7719667503f4b01
|
||||
fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b
|
||||
fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \
|
||||
include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
- [ ] S-08 刚编辑过幻灯片就进备注栏键入,备注可能丢
|
||||
- [ ] S-09 改尺寸失败后 `size_asked` 一直留着
|
||||
- [ ] S-10 开始放映的两处拒绝无提示(规则 2)
|
||||
- [ ] P-01 文件系统的 `PermissionDenied` 被拼成不存在的文案键,应用崩溃
|
||||
- [x] P-01 文件系统的 `PermissionDenied` 被拼成不存在的文案键,应用崩溃 — worker 的拒绝改为自有错误类型 Refused;R1 测试不跨平台、不看提示;组件库 f834f55 加 Toaster::shown;R2 PASS
|
||||
- [ ] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失
|
||||
- [ ] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图
|
||||
- [ ] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
apps/ely-pdf/src/tests/output.rs:38:新增测试直接使用 Unix 专属的 `std::os::unix::fs::PermissionsExt` 和 `Permissions::from_mode`,测试函数与上层测试模块均缺少平台条件。Windows 编译 ElyPDF 测试目标时会报 E0433、E0599,阻断该平台测试。已将这两处用法原样交给已安装的 `x86_64-pc-windows-msvc` 目标执行 `rustc --emit metadata`,复现两项错误,证据见 `/tmp/ely-p01-r1-windows.log`。修复方向:用 `#[cfg(unix)]` 限定该 Unix 权限测试,保证 Windows 测试目标能够编译。
|
||||
|
||||
apps/ely-pdf/src/tests/output.rs:55:新增权限错误回归测试只断言目标文件不存在,固定推进帧后结束,缺少失败处理完成与用户可见错误的断言,静默吞错仍能通过,漏检规则 2 与 PRD UX-05、UX-06 要求的错误反馈。在独立源码副本中将 `Viewer::failed` 改为仅记录探针日志后返回,实际收到 `PermissionDenied`,此测试仍为 `1 passed`;证据见 `/tmp/ely-p01-r1-mutation.log` 与 `/tmp/ely-p01-r1-observed-error.log`。修复方向:接入真实错误提示接收者,等待失败处理完成并断言权限错误提示,使吞错变异触发测试失败。
|
||||
@@ -0,0 +1 @@
|
||||
PASS
|
||||
Reference in new issue
Block a user