fix: block frontmatter writes for attachments

This commit is contained in:
lucaronin
2026-04-30 05:37:58 +02:00
parent 388d389ba4
commit bc56eec35e
3 changed files with 90 additions and 21 deletions

View File

@@ -9,15 +9,36 @@ use std::path::Path;
pub use ops::update_frontmatter_content;
pub use yaml::{format_yaml_key, FrontmatterValue};
fn is_markdown_path(path: &Path) -> bool {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown")
})
}
fn validate_frontmatter_path(path: &str, file_path: &Path) -> Result<(), String> {
if !file_path.exists() {
return Err(format!("File does not exist: {}", path));
}
if !is_markdown_path(file_path) {
return Err(format!(
"Frontmatter can only be updated on Markdown notes: {}",
path
));
}
Ok(())
}
/// Helper to read a file, apply a frontmatter transformation, and write back.
pub fn with_frontmatter<F>(path: &str, transform: F) -> Result<String, String>
where
F: FnOnce(&str) -> Result<String, String>,
{
let file_path = Path::new(path);
if !file_path.exists() {
return Err(format!("File does not exist: {}", path));
}
validate_frontmatter_path(path, file_path)?;
let content =
fs::read_to_string(file_path).map_err(|e| format!("Failed to read {}: {}", path, e))?;
@@ -58,6 +79,25 @@ mod tests {
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn test_update_frontmatter_rejects_binary_attachment_before_utf8_read() {
let dir = tempfile::tempdir().unwrap();
let attachment_dir = dir.path().join("attachments");
fs::create_dir_all(&attachment_dir).unwrap();
let attachment_path = attachment_dir.join("screenshot.png");
fs::write(&attachment_path, [0xff, 0xfe, 0xfd]).unwrap();
let err = update_frontmatter(
attachment_path.to_str().unwrap(),
"Status",
FrontmatterValue::String("Done".to_string()),
)
.unwrap_err();
assert!(err.contains("Frontmatter can only be updated on Markdown notes"));
assert!(err.contains("screenshot.png"));
}
#[test]
fn test_roundtrip_update_string() {
let content = "---\nStatus: Draft\n---\n# Test\n";

View File

@@ -634,6 +634,29 @@ Status: Active
expect(onInit).toHaveBeenCalledWith('/vault/plain-note.md')
})
it('does not offer frontmatter initialization for binary attachments', () => {
const onInit = vi.fn()
const attachmentEntry: VaultEntry = {
...noFrontmatterEntry,
path: '/vault/attachments/screenshot.png',
filename: 'screenshot.png',
title: 'screenshot.png',
fileKind: 'binary',
}
render(
<Inspector
{...defaultProps}
entry={attachmentEntry}
content=""
onInitializeProperties={onInit}
/>
)
expect(screen.queryByText('Initialize properties')).not.toBeInTheDocument()
expect(onInit).not.toHaveBeenCalled()
})
it('shows invalid frontmatter notice with fix button', () => {
render(
<Inspector

View File

@@ -48,6 +48,10 @@ function buildTypeEntryMap(entries: VaultEntry[]): Record<string, VaultEntry> {
return map
}
function supportsFrontmatter(entry: VaultEntry): boolean {
return entry.fileKind === undefined || entry.fileKind === 'markdown'
}
function ValidFrontmatterPanels({
entry,
entries,
@@ -212,24 +216,26 @@ function InspectorBody({
return (
<>
<PrimaryInspectorPanel
entry={entry}
frontmatterState={frontmatterState}
frontmatter={frontmatter}
entries={entries}
typeEntryMap={typeEntryMap}
vaultPath={vaultPath}
referencedBy={referencedBy}
onNavigate={onNavigate}
onToggleRawEditor={onToggleRawEditor}
onInitializeProperties={onInitializeProperties}
onCreateAndOpenNote={onCreateAndOpenNote}
onUpdateProperty={onUpdateFrontmatter ? handleUpdateProperty : undefined}
onDeleteProperty={onDeleteProperty ? handleDeleteProperty : undefined}
onAddProperty={onAddProperty ? handleAddProperty : undefined}
onCreateMissingType={onCreateMissingType ? handleCreateMissingType : undefined}
locale={locale}
/>
{supportsFrontmatter(entry) && (
<PrimaryInspectorPanel
entry={entry}
frontmatterState={frontmatterState}
frontmatter={frontmatter}
entries={entries}
typeEntryMap={typeEntryMap}
vaultPath={vaultPath}
referencedBy={referencedBy}
onNavigate={onNavigate}
onToggleRawEditor={onToggleRawEditor}
onInitializeProperties={onInitializeProperties}
onCreateAndOpenNote={onCreateAndOpenNote}
onUpdateProperty={onUpdateFrontmatter ? handleUpdateProperty : undefined}
onDeleteProperty={onDeleteProperty ? handleDeleteProperty : undefined}
onAddProperty={onAddProperty ? handleAddProperty : undefined}
onCreateMissingType={onCreateMissingType ? handleCreateMissingType : undefined}
locale={locale}
/>
)}
{backlinks.length > 0 && <Separator />}
<BacklinksPanel backlinks={backlinks} onNavigate={onNavigate} />
<Separator />