{"record":{"id":"f6a63e7eb66b821e","repo":"sxyazi/yazi","slug":"empty-file-stem","errorCode":null,"errorMessage":"empty file stem","messagePattern":"empty file stem","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"yazi-vfs/src/fns.rs","lineNumber":28,"sourceCode":"\t\tOk(_) => true,\n\t\tErr(e) => e.kind() != io::ErrorKind::NotFound,\n\t}\n}\n\npub async fn unique_file(u: UrlBuf, is_dir: bool) -> io::Result<UrlBuf> {\n\tlet result =\n\t\tif is_dir { engine::create_dir(&u).await } else { engine::create_new(&u).await.map(|_| ()) };\n\n\tmatch result {\n\t\tOk(()) => Ok(u),\n\t\tErr(e) if e.kind() == io::ErrorKind::AlreadyExists => _unique_file(u, is_dir).await,\n\t\tErr(e) => Err(e),\n\t}\n}\n\nasync fn _unique_file(mut url: UrlBuf, is_dir: bool) -> io::Result<UrlBuf> {\n\tlet Some(stem) = url.stem().owned() else {\n\t\treturn Err(io::Error::new(io::ErrorKind::InvalidInput, \"empty file stem\"));\n\t};\n\n\tlet dot_ext = match url.ext() {\n\t\tSome(e) => {\n\t\t\tlet mut s = StrandBuf::with_capacity(url.loc().kind(), e.len() + 1);\n\t\t\ts.push_str(\".\");\n\t\t\ts.try_push(e)?;\n\t\t\ts\n\t\t}\n\t\tNone => StrandBuf::default(),\n\t};\n\n\tlet mut name = StrandBuf::with_capacity(url.loc().kind(), stem.len() + dot_ext.len() + 5);\n\tfor i in 1u64.. {\n\t\tname.clear();\n\t\tname.try_push(&stem)?;\n\n\t\tif is_dir {","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/sxyazi/yazi/blob/8ebf930f1796ae2076b7d6b0c5e93a1002c18134/yazi-vfs/src/fns.rs#L10-L46","documentation":"`_unique_file` generates a non-colliding filename (e.g. `name (1).ext`) for copy/paste or download dedup, and it requires the URL's file stem to be non-empty. If `url.stem()` yields None — a path ending in a separator, a dot-only tail, or otherwise empty stem — the function aborts with InvalidInput instead of inventing a name.","triggerScenarios":"Calling `unique_file(url, is_dir)` with a URL whose tail has no stem: a trailing slash (directory tail without a name), a name consisting only of an extension separator, or an empty last component produced by joining paths programmatically.","commonSituations":"Pasting/downloads where the remote name was empty or slash-terminated, drag-drop payloads with bare directory URLs, or code constructing `dir.join(\"\")` and passing the result to unique_file.","solutions":["Strip trailing separators and ensure the URL has a real file name before calling unique_file","If the source has no name, synthesize one (e.g. 'download', 'folder') before invoking the API","Validate with `url.file_name()`/stem on the caller side and return a clear user-facing error otherwise","Check upstream producers (clipboard, download job) for why the name is empty"],"exampleFix":"-- before\nlocal unique = vfs.unique_file(url) -- url ends with '/'\n-- after\nif not url:stem() then url = url:join('unnamed') end\nlocal unique = vfs.unique_file(url)","handlingStrategy":"validation","validationCode":"local tail = url:name() or url:urn()\nassert(tail ~= '' and tail ~= '/' and url:stem(), 'unique_file needs a non-empty file stem, got: '..tostring(tail))","typeGuard":null,"tryCatchPattern":"local ok, unique = pcall(function() return vfs.unique_file(url, is_dir) end)\nif not ok then\n  -- 'empty file stem' (InvalidInput): synthesize a name and retry\n  url = url:join('unnamed')\n  unique = vfs.unique_file(url, is_dir)\nend","preventionTips":["Strip trailing separators from URLs before dedup/name-generation calls","Never pass empty-tailed or programmatically joined-empty URLs to unique_file","Synthesize fallback names ('download', 'unnamed') when the source provides none","Audit producers (clipboard paste, download jobs) to guarantee they always yield a named entry"],"tags":["vfs","filename","validation","invalid-input"],"backgroundTag":"empty-file-stem","analyzedSha":"8ebf930f1796ae2076b7d6b0c5e93a1002c18134","analyzedAt":"2026-09-02T18:38:25.566Z","contentChangedAt":"2026-09-02T18:38:25.566Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}