{"record":{"id":"a712a9df36caa261","repo":"jdx/mise","slug":"app-info-plist-must-be-a-regular-file","errorCode":null,"errorMessage":"app Info.plist must be a regular file","messagePattern":"app Info\\.plist must be a regular file","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/app_version.rs","lineNumber":60,"sourceCode":"\n/// Reads optional short and build strings from an absolute app bundle path.\n///\n/// Accepts XML and binary plists. Returns an error for unsafe paths, nonregular\n/// files, malformed dictionaries, or version fields whose values are not strings.\npub(super) fn read_app_version(app: &Path) -> Result<AppVersion> {\n    use nix::fcntl::{OFlag, openat};\n    use nix::sys::stat::{Mode, SFlag, fstat};\n\n    let contents = app.join(\"Contents\");\n    let parent = open_trusted_directory(Path::new(\"/\"), contents.strip_prefix(\"/\")?, true, false)?;\n    let fd = openat(\n        &parent.fd,\n        \"Info.plist\",\n        OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_NONBLOCK,\n        Mode::empty(),\n    )?;\n    if SFlag::from_bits_truncate(fstat(&fd)?.st_mode) & SFlag::S_IFMT != SFlag::S_IFREG {\n        bail!(\"app Info.plist must be a regular file\");\n    }\n    let plist = plist::Value::from_reader(std::fs::File::from(fd))?;\n    let dict = plist\n        .as_dictionary()\n        .ok_or_else(|| eyre!(\"app Info.plist must be a dictionary\"))?;\n    let field = |key| -> Result<Option<String>> {\n        dict.get(key)\n            .map(|value| {\n                value\n                    .as_string()\n                    .map(str::to_owned)\n                    .ok_or_else(|| eyre!(\"app {key} must be a string\"))\n            })\n            .transpose()\n    };\n    Ok(AppVersion {\n        short: field(\"CFBundleShortVersionString\")?,\n        build: field(\"CFBundleVersion\")?,","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/packages/brew/cask/app_version.rs#L42-L78","documentation":"read_app_version opens a cask app bundle's Info.plist with O_NOFOLLOW and then verifies via fstat that the opened file is a regular file (S_IFREG). If it is not — e.g. a symlink, directory, FIFO, or device — the function bails instead of reading it, a hardening measure against symlink-based path tricks and unreadable/odd file types in .app bundles.","triggerScenarios":"Calling read_app_version (directly or via installed_skip_reason, or during brew cask install/state checks) on an app whose <App>.app/Contents/Info.plist is not a regular file: a symlinked plist, a directory named Info.plist, a broken/odd filesystem entry, or a maliciously crafted bundle.","commonSituations":"Corrupted or hand-assembled .app bundles in ~/Applications or /Applications; Homebrew casks whose app target was replaced by a symlink (e.g. pointing Info.plist into a shared resources dir); network mounts or exotic filesystems returning non-regular inode types; tampered bundles flagged during security checks.","solutions":["Inspect the plist path: ls -l '<App>.app/Contents/Info.plist' and replace it with a real regular file (e.g. restore the app by reinstalling the cask: brew reinstall --cask <token>).","Remove any symlink: delete the symlink and copy the real Info.plist into place, or reinstall the application that owns the bundle.","If the app sits on a network/exotic mount, move it to a local APFS/HFS+ volume so Info.plist is a regular file.","If you intentionally test with odd file types, expect this error — it is a deliberate safety rejection, not a bug."],"exampleFix":"// before (shell): Info.plist is a symlink\n$ ls -l MyApp.app/Contents/Info.plist\nInfo.plist -> /shared/plists/MyApp.plist\n// after\n$ rm MyApp.app/Contents/Info.plist\n$ cp /shared/plists/MyApp.plist MyApp.app/Contents/Info.plist\n$ brew reinstall --cask myapp   # or let mise re-read the version","handlingStrategy":"validation","validationCode":"import std::fs;\nuse std::os::unix::fs::FileTypeExt;\nlet meta = std::fs::symlink_metadata(format!(\"{}.app/Contents/Info.plist\", app_path))?;\nlet ok = !meta.file_type().is_symlink() && meta.is_file();\nif !ok { eprintln!(\"Info.plist must be a regular file (no symlinks): {app_path}\"); }","typeGuard":"fn is_regular_plist(path: &std::path::Path) -> bool {\n    std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)\n}","tryCatchPattern":"match read_app_version(&app) {\n    Ok(v) => println!(\"version {v}\"),\n    Err(e) if e.to_string().contains(\"must be a regular file\") => {\n        eprintln!(\"App bundle is corrupt or tampered; reinstall the cask.\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never symlink Info.plist inside .app bundles; keep bundles intact.","Reinstall casks after manually moving or rsyncing apps between volumes.","Watch for apps restored from backups with changed file types; verify with ls -l before relying on version reads.","Treat this error as a tamper/corruption signal rather than trying to bypass the check."],"tags":["macos","brew-cask","filesystem","security","plist"],"backgroundTag":"file-read-failed","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}