jdx/mise · error

app Info.plist must be a regular file

Error message

app Info.plist must be a regular file

What it means

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.

Source

Thrown at src/system/packages/brew/cask/app_version.rs:60

/// Reads optional short and build strings from an absolute app bundle path.
///
/// Accepts XML and binary plists. Returns an error for unsafe paths, nonregular
/// files, malformed dictionaries, or version fields whose values are not strings.
pub(super) fn read_app_version(app: &Path) -> Result<AppVersion> {
    use nix::fcntl::{OFlag, openat};
    use nix::sys::stat::{Mode, SFlag, fstat};

    let contents = app.join("Contents");
    let parent = open_trusted_directory(Path::new("/"), contents.strip_prefix("/")?, true, false)?;
    let fd = openat(
        &parent.fd,
        "Info.plist",
        OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_NONBLOCK,
        Mode::empty(),
    )?;
    if SFlag::from_bits_truncate(fstat(&fd)?.st_mode) & SFlag::S_IFMT != SFlag::S_IFREG {
        bail!("app Info.plist must be a regular file");
    }
    let plist = plist::Value::from_reader(std::fs::File::from(fd))?;
    let dict = plist
        .as_dictionary()
        .ok_or_else(|| eyre!("app Info.plist must be a dictionary"))?;
    let field = |key| -> Result<Option<String>> {
        dict.get(key)
            .map(|value| {
                value
                    .as_string()
                    .map(str::to_owned)
                    .ok_or_else(|| eyre!("app {key} must be a string"))
            })
            .transpose()
    };
    Ok(AppVersion {
        short: field("CFBundleShortVersionString")?,
        build: field("CFBundleVersion")?,

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. 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>).
  2. Remove any symlink: delete the symlink and copy the real Info.plist into place, or reinstall the application that owns the bundle.
  3. If the app sits on a network/exotic mount, move it to a local APFS/HFS+ volume so Info.plist is a regular file.
  4. If you intentionally test with odd file types, expect this error — it is a deliberate safety rejection, not a bug.

Example fix

// before (shell): Info.plist is a symlink
$ ls -l MyApp.app/Contents/Info.plist
Info.plist -> /shared/plists/MyApp.plist
// after
$ rm MyApp.app/Contents/Info.plist
$ cp /shared/plists/MyApp.plist MyApp.app/Contents/Info.plist
$ brew reinstall --cask myapp   # or let mise re-read the version
Defensive patterns

Strategy: validation

Validate before calling

import std::fs;
use std::os::unix::fs::FileTypeExt;
let meta = std::fs::symlink_metadata(format!("{}.app/Contents/Info.plist", app_path))?;
let ok = !meta.file_type().is_symlink() && meta.is_file();
if !ok { eprintln!("Info.plist must be a regular file (no symlinks): {app_path}"); }

Type guard

fn is_regular_plist(path: &std::path::Path) -> bool {
    std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false)
}

Try / catch

match read_app_version(&app) {
    Ok(v) => println!("version {v}"),
    Err(e) if e.to_string().contains("must be a regular file") => {
        eprintln!("App bundle is corrupt or tampered; reinstall the cask.");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/a712a9df36caa261. Report an issue: GitHub.