{"record":{"id":"f476d6e602718b63","repo":"siyuan-note/siyuan","slug":"invalid-notebook-id-f476d6","errorCode":null,"errorMessage":"invalid notebook ID","messagePattern":"invalid notebook ID","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/mount.go","lineNumber":188,"sourceCode":"\t}\n\treturn\n}\n\nfunc collectBoxDeletedAttributeViewBlocks(boxID string) (ret map[string]map[string]struct{}, err error) {\n\trootIDs := treenode.GetRootBlockIDsByBoxID(boxID)\n\tif 1 > len(rootIDs) {\n\t\treturn map[string]map[string]struct{}{}, nil\n\t}\n\tboundAVIDs, err := sql.QueryBoundBlockAVIDsInBox(nil, rootIDs, boxID)\n\tif nil != err {\n\t\treturn nil, err\n\t}\n\treturn groupDeletedAttributeViewBlocks(boundAVIDs), nil\n}\n\nfunc RemoveBox(boxID string) (err error) {\n\tif !ast.IsNodeIDPattern(boxID) {\n\t\treturn errors.New(\"invalid notebook ID\")\n\t}\n\tif _, loaded := boxLock.LoadOrStore(boxID, true); loaded {\n\t\terr = errors.New(Conf.language(239))\n\t\treturn\n\t}\n\tdefer boxLock.Delete(boxID)\n\n\tif util.IsReservedFilename(boxID) {\n\t\treturn fmt.Errorf(\"can not remove [%s] caused by it is a reserved file\", boxID)\n\t}\n\n\tFlushTxQueue()\n\tsql.FlushQueue()\n\t// 索引和笔记本目录删除后无法再读取 custom-avs，需提前收集；实际删除成功后再清理绑定行。\n\tdeletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)\n\tif nil != err {\n\t\treturn fmt.Errorf(\"query database-bound blocks in notebook [%s] failed: %w\", boxID, err)\n\t}","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/mount.go#L170-L206","documentation":"First guard in RemoveBox: if !ast.IsNodeIDPattern(boxID) the function returns a raw errors.New('invalid notebook ID') (not localised). ast.IsNodeIDPattern validates that the ID matches SiYuan's node-ID format (the format generated by ast.NewNodeID). This rejects arbitrary/garbage IDs before any filesystem or database work.","triggerScenarios":"Calling RemoveBox with a boxID that is not a valid SiYuan node ID: empty string, a human name, a path, a UUID, a truncated/altered ID, or any string failing the node-ID regex. The check runs before the boxLock and reserved-filename checks.","commonSituations":"Programmatic caller passing a UI label instead of an ID; ID read from an untrusted source; copy-paste truncation; integration tests using dummy IDs like 'test' or '1'; confusion between notebook name and notebook ID.","solutions":["Always pass the notebook ID returned by CreateBox/ListNotebooks, not its display name.","Validate with ast.IsNodeIDPattern(boxID) before calling RemoveBox and reject early.","If exposing removal via an API, normalise and validate the ID at the trust boundary."],"exampleFix":"// before\nerr := model.RemoveBox(\"My Notebook\") // 'invalid notebook ID'\n\n// after\nif !ast.IsNodeIDPattern(boxID) {\n    return fmt.Errorf(\"invalid notebook ID: %q\", boxID)\n}\nerr := model.RemoveBox(boxID)","handlingStrategy":"validation","validationCode":"if !ast.IsNodeIDPattern(boxID) {\n    return fmt.Errorf(\"invalid notebook ID: %q\", boxID)\n}","typeGuard":"// isNotebookID narrows a string to a valid SiYuan notebook ID.\nfunc isNotebookID(id string) bool { return ast.IsNodeIDPattern(id) }","tryCatchPattern":"null","preventionTips":["Always use IDs from CreateBox/ListNotebooks, never display names.","Validate IDs at API trust boundaries with ast.IsNodeIDPattern."],"tags":["notebook","mount","remove","validation","id-format"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}