{"record":{"id":"b57032e36b49f06d","repo":"siyuan-note/siyuan","slug":"hidden-template-paths-are-reserved","errorCode":null,"errorMessage":"hidden template paths are reserved","messagePattern":"hidden template paths are reserved","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_manage.go","lineNumber":65,"sourceCode":"}\n\ntype TemplateFileEntry struct {\n\tPath      string `json:\"path\"`\n\tIsDir     bool   `json:\"isDir\"`\n\tIsPackage bool   `json:\"isPackage,omitempty\"`\n}\n\n// 访问已有模板只校验目录边界，不对文件名进行清理或改写。\nfunc validateTemplateRelativePath(p string, allowRoot bool) error {\n\tif p == \"\" && allowRoot {\n\t\treturn nil\n\t}\n\tif p == \"\" || !fs.ValidPath(p) || strings.ContainsAny(p, \"\\\\:\\x00\") {\n\t\treturn errors.New(\"invalid template path\")\n\t}\n\tfor _, part := range strings.Split(p, \"/\") {\n\t\tif strings.HasPrefix(part, \".\") {\n\t\t\treturn errors.New(\"hidden template paths are reserved\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// 新名称保持跨平台可用，已有父目录沿用原名。\nfunc validateNewTemplateName(p string) error {\n\tpart := path.Base(p)\n\tdevice := strings.ToUpper(strings.SplitN(part, \".\", 2)[0])\n\tif device == \"CON\" || device == \"PRN\" || device == \"AUX\" || device == \"NUL\" || (len(device) == 4 && (strings.HasPrefix(device, \"COM\") || strings.HasPrefix(device, \"LPT\")) && device[3] >= '1' && device[3] <= '9') {\n\t\treturn errors.New(\"reserved template file name\")\n\t}\n\tif strings.HasPrefix(part, \".\") || strings.TrimSpace(part) != part || strings.HasSuffix(part, \".\") || strings.ContainsAny(part, \"\\\\:<>\\\"|?*\") || strings.ContainsFunc(part, unicode.IsControl) {\n\t\treturn errors.New(\"invalid template path component\")\n\t}\n\treturn nil\n}\n","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_manage.go#L47-L83","documentation":"Any path segment of the template relative path starting with a dot is rejected as reserved for hidden entries. Hidden files/directories are excluded from the template namespace to avoid colliding with metadata or dotfiles on any platform.","triggerScenarios":"validateTemplateRelativePath (via DocSaveAsTemplateInDirectory or checkTemplateFilePath) receives a path where strings.HasPrefix(part, \".\") for some segment, e.g. \".config/tpl.md\" or \"notes/.hidden.md\".","commonSituations":"Users trying to store templates in a dot-directory; hidden temp files (e.g. \".DS_Store\", editor swap files) being passed as template paths; programmatic paths built from hidden config dirs.","solutions":["Remove or rename the dot-prefixed segment so no path part starts with \".\"","Store templates in normal (non-hidden) directories","Filter out dotfiles before iterating template candidates"],"exampleFix":"// before\nerr := validateTemplateRelativePath(\".hidden/tpl.md\", false)\n// after\nerr := validateTemplateRelativePath(\"templates/tpl.md\", false)\n","handlingStrategy":"validation","validationCode":"func hasHiddenSegment(p string) bool {\n    for _, part := range strings.Split(p, \"/\") {\n        if strings.HasPrefix(part, \".\") { return true }\n    }\n    return false\n}\n// reject before calling: hasHiddenSegment(p)","typeGuard":null,"tryCatchPattern":"if hasHiddenSegment(p) {\n    return errors.New(\"template paths must not contain hidden (dot-prefixed) segments\")\n}\nif err := validateTemplateRelativePath(p, false); err != nil { return err }","preventionTips":["Avoid dot-prefixed files/directories in the template tree","Skip hidden files when iterating template candidates","Educate users that templates live in normal visible directories"],"tags":["template","path","hidden-files"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}