{"record":{"id":"29c32bb56d255bba","repo":"jdx/mise","slug":"copy-image-path-image-path-ends-with-but","errorCode":null,"errorMessage":"copy image path {image_path:?} ends with `/` but {} is not a directory; specify the exact destination file name","messagePattern":"copy image path (.+?) ends with `/` but (.+?) is not a directory; specify the exact destination file name","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/layer.rs","lineNumber":126,"sourceCode":"/// Directory contents are placed under `image_path`; a file or symlink is\n/// placed at `image_path` itself.\npub fn build_layer_from_path(\n    host_path: &Path,\n    image_path: &str,\n    owner: LayerOwner,\n) -> Result<LayerBlob> {\n    let metadata = std::fs::symlink_metadata(host_path)\n        .wrap_err_with(|| format!(\"reading metadata for {}\", host_path.display()))?;\n    let target = image_path.trim_matches('/');\n\n    if metadata.is_dir() {\n        return build_layer_from_dir(host_path, target, owner);\n    }\n    if target.is_empty() {\n        eyre::bail!(\"cannot copy a file or symlink to the image root\");\n    }\n    if image_path.ends_with('/') {\n        eyre::bail!(\n            \"copy image path {image_path:?} ends with `/` but {} is not a directory; \\\n             specify the exact destination file name\",\n            host_path.display()\n        );\n    }\n\n    let kind = if metadata.file_type().is_symlink() {\n        EntryKind::Symlink(\n            std::fs::read_link(host_path)\n                .wrap_err_with(|| format!(\"reading symlink {}\", host_path.display()))?,\n        )\n    } else if metadata.is_file() {\n        EntryKind::File\n    } else {\n        eyre::bail!(\"unsupported host path type: {}\", host_path.display());\n    };\n    let mode = match &kind {\n        EntryKind::Symlink(_) => 0o777,","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/layer.rs#L108-L144","documentation":"In tar-copy semantics a trailing '/' on the destination means 'this is a directory target' (like cp -r dir/). The guard fires when the source's symlink_metadata is NOT a directory (file or symlink) but image_path still ends with '/', because then the intended file name would be ambiguous — the entry would otherwise be placed at an unspecified name inside that directory.","triggerScenarios":"A copy entry pairing a regular file or symlink source with a destination like \"/usr/local/bin/\" — the code path is reached only when metadata.is_dir() is false, then the trailing slash is rejected.","commonSituations":"Copy-pasting a directory-style destination for a file, or template code that appends '/' to every destination uniformly.","solutions":["Remove the trailing slash and spell the full destination file name: \"./bin/mytool:/usr/local/bin/mytool\".","Or change the source to the parent directory if you really want the whole tree copied."],"exampleFix":"# before\nfiles = [ \"./config.toml:/etc/mise/\" ]\n\n# after\nfiles = [ \"./config.toml:/etc/mise/config.toml\" ]","handlingStrategy":"validation","validationCode":"import os, sys\nfor src, dst in copy_entries.items():\n    if dst.endswith(\"/\") and not os.path.isdir(src):\n        sys.exit(f\"{src} is a file but {dst!r} ends with '/'; give the exact file name\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Mirror cp semantics: trailing slash only for directory targets.","Prefer writing the full destination filename everywhere; it is unambiguous for both kinds.","Avoid templating that unconditionally appends '/' to destinations."],"tags":["oci","layer","path-mismatch","trailing-slash"],"backgroundTag":"path-type-mismatch","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}