{"record":{"id":"f71b798454a87c74","repo":"siyuan-note/siyuan","slug":"reserved-template-file-name","errorCode":null,"errorMessage":"reserved template file name","messagePattern":"reserved template file name","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_manage.go","lineNumber":76,"sourceCode":"\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\n// 清单与目录共同标识模板包，保留目录身份以维持搜索和集市更新。\nfunc isManagedTemplatePackage(root *os.Root, p string) bool {\n\t_, err := root.Lstat(path.Join(p, \"template.json\"))\n\treturn err == nil\n}\n\nfunc openTemplateRoot() (*os.Root, error) {\n\tif err := os.MkdirAll(filepath.Join(util.DataDir, \"templates\"), 0755); err != nil {\n\t\treturn nil, err\n\t}\n\treturn os.OpenRoot(filepath.Join(util.DataDir, \"templates\"))","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_manage.go#L58-L94","documentation":"validateNewTemplateName rejects file names whose base (before the first dot, uppercased) is a Windows reserved device name: CON, PRN, AUX, NUL, or COM1-9/LPT1-9. Such names cannot be created as files on Windows, so they are refused up front for cross-platform safety.","triggerScenarios":"ManageTemplateFiles calls validateNewTemplateName with a new template name whose path.Base matches a reserved device (case-insensitive), e.g. \"con.md\", \"COM1.md\", \"lpt3.md\".","commonSituations":"Users naming a template \"con\" or \"aux\" (works on Unix, breaks on Windows); automated generation of names from untrusted input; migration from a Unix-only setup to Windows.","solutions":["Choose a file name that is not a Windows reserved device name (append a word or suffix, e.g. \"con-template.md\")","Validate user-supplied names in the UI before calling ManageTemplateFiles","Add a name sanitizer that prefixes reserved names"],"exampleFix":"// before\nname := \"con.md\"\nerr := validateNewTemplateName(name) // fails\n// after\nname := \"con-template.md\"\nerr := validateNewTemplateName(name) // ok\n","handlingStrategy":"validation","validationCode":"func isReservedDeviceName(name string) bool {\n    d := strings.ToUpper(strings.SplitN(path.Base(name), \".\", 2)[0])\n    if d == \"CON\" || d == \"PRN\" || d == \"AUX\" || d == \"NUL\" { return true }\n    return len(d) == 4 && (strings.HasPrefix(d, \"COM\") || strings.HasPrefix(d, \"LPT\")) && d[3] >= '1' && d[3] <= '9'\n}","typeGuard":null,"tryCatchPattern":"if isReservedDeviceName(newName) {\n    newName = \"tpl-\" + newName // sanitize before validating\n}\nif err := validateNewTemplateName(newName); err != nil { return err }","preventionTips":["Never name templates CON, PRN, AUX, NUL, COM1-9, or LPT1-9","Sanitize user-supplied names against reserved devices in the UI","Test name handling on Windows as well as Unix"],"tags":["template","naming","windows-reserved-names"],"backgroundTag":"invalid-argument-value","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"}