AprilNEA/OpenLogi · error

fixture directory has no UTF-8 synthetic ID

Error message

fixture directory has no UTF-8 synthetic ID

What it means

Thrown by `publish_manifest_and_finish` during fixture contribution when the fixture directory's `file_name()` cannot be converted to a UTF-8 string (`OsStr::to_str` returns None). The directory's final path component doubles as the synthetic fixture ID in the manifest, so a non-UTF-8 name makes an exact, stable fixture ID impossible. This is a filesystem-encoding invariant on the contribution path.

Solutions

  1. Rename the fixture directory to a UTF-8 name (ASCII recommended) and re-run the contribute/finish step.
  2. Check the name with `printf '%s' "$dir" | xxd | tail` or `ls | LC_ALL=C grep -P '[\x80-\xff]'` to spot invalid bytes.
  3. Re-extract or re-create the fixture directory from the source, forcing UTF-8 filenames.
  4. If this comes from an automated pipeline, make it generate fixture IDs from `[A-Za-z0-9_-]` only.

Example fix

// before: non-UTF-8 directory name
$ openlogi fixture contribute finish /tmp/fixtures/caf$é-latin1
// error: fixture directory has no UTF-8 synthetic ID
// after: rename to ASCII UTF-8
$ mv /tmp/fixtures/caf$'\xe9'-latin1 /tmp/fixtures/cafe-latin1
$ openlogi fixture contribute finish /tmp/fixtures/cafe-latin1
Defensive patterns

Strategy: validation

Validate before calling

// validate the fixture directory name is UTF-8 before starting the contribute flow
fn validate_fixture_dir(path: &Path) -> Result<(), String> {
    match path.file_name().and_then(|n| n.to_str()) {
        Some(name) if name.is_ascii() => Ok(()),
        _ => Err(format!("fixture dir {:?} must have a UTF-8 (ASCII-safe) name", path)),
    }
}

Type guard

fn has_utf8_id(path: &std::path::Path) -> bool {
    path.file_name().map(|n| n.to_str().is_some()).unwrap_or(false)
}

Try / catch

match finish(directory) {
    Err(e) if msg_contains(&e, "no UTF-8 synthetic ID") => {
        eprintln!("rename the fixture directory to a UTF-8 name and retry: {e:#}");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running the fixture contribute/finish flow against a directory whose final path component contains invalid UTF-8 bytes (possible on Linux where paths are arbitrary bytes, e.g. Latin-1 filenames created by other tools).

Common situations: Contributing a fixture directory created or extracted by a tool that wrote non-UTF-8 filenames; a directory name with raw bytes from a zip/tar with a legacy encoding; renaming automation that mangled the name.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of AprilNEA/OpenLogi@e846e6f4b4 (2026-09-13). Data as JSON: /api/errors/b0a30997cdffcf38. Report an issue: GitHub.

Appendix: source

Thrown at crates/openlogi-cli/src/cmd/fixture/contribute.rs:244

        args.output.display()
    );
    println!(
        "No data was uploaded; physical and semantic correctness still require maintainer review."
    );
    Ok(())
}

fn publish_manifest_and_finish(
    directory: &Path,
    state_path: &Path,
    profile: &DeviceProfile,
    cassettes: &[HidCassette],
    bindings: &[FixtureCaseBinding],
) -> Result<()> {
    let fixture_id = directory
        .file_name()
        .and_then(OsStr::to_str)
        .ok_or_else(|| anyhow!("fixture directory has no UTF-8 synthetic ID"))?;
    let manifest =
        FixtureManifest::from_assets(fixture_id.to_string(), profile, cassettes, bindings)
            .context("could not generate an exact fixture manifest from sanitized assets")?;
    validate_resumable_layout(directory, cassettes)?;

    if !cassettes.is_empty() {
        let cases_directory = directory.join(CASES_DIRECTORY);
        if cases_directory
            .try_exists()
            .context("could not inspect cases directory")?
        {
            require_directory(&cases_directory, "fixture cases directory")?;
        }
        for cassette in cassettes {
            let path = cases_directory.join(format!("{}.json", cassette.name));
            reject_symlink(&path, "fixture cassette")?;
            super::output::write_json_atomically(&path, cassette, true, "HID cassette")?;
        }

View on GitHub (pinned to e846e6f4b4)