Skip to content

Fix FMI 2.0 resource location parsing in the Chrono::Vehicle FMUs - #772

Open
DanNegrut wants to merge 2 commits into
mainfrom
fix/fmi2-resource-location-uri
Open

Fix FMI 2.0 resource location parsing in the Chrono::Vehicle FMUs#772
DanNegrut wants to merge 2 commits into
mainfrom
fix/fmi2-resource-location-uri

Conversation

@DanNegrut

Copy link
Copy Markdown
Contributor

Fixes #762.

Problem

The FMI 2.0 standard specifies that the fmuResourceLocation argument of fmi2Instantiate is a URI (RFC 3986/8089) pointing to the resources directory of the unpacked FMU, not a plain filesystem path. All six Chrono::Vehicle FMUs converted it by removing a fixed-length prefix:

auto resources_dir = std::string(fmuResourceLocation).erase(0, 8);

Chopping 8 characters removes file:///. On Windows that is correct, because the drive letter follows the third slash and file:///C:/temp becomes C:/temp. On POSIX it removes the leading slash of an absolute path, so file:///tmp/xyz/resources becomes the relative tmp/xyz/resources and the JSON specification files are not found.

Why this went unnoticed

Chrono's own importer does not emit a conforming URI. fmu-forge builds the location in FmuUnit::Instantiate as

"file:///" + m_directory + "/resources"

and on POSIX m_directory is already absolute, so the result carries four slashes (file:////tmp/...). A blind 8-character chop happens to land correctly on that. A standards-conforming importer such as FMPy, Dymola or OpenModelica emits three slashes, and the chop then eats the root slash. The practical effect is that the Chrono vehicle FMUs work on Linux and macOS only with Chrono's own loader, which defeats the purpose of shipping FMI components.

Change

Replace the prefix removal at all six call sites with a shared chrono::fmi2::GetResourcesPath(), in a new header with no fmu-forge dependency so it can be included and tested on its own. ChFmuToolsExport.h includes it, so the call sites change only in the call itself.

The parser handles an empty or localhost authority (matched case-insensitively, since host names are), the Windows drive-letter form, the RFC 8089 minimal file:/path form, repeated leading slashes so the existing four-slash form keeps working, percent-encoded octets, and a plain path for importers that pass one instead of a URI. Interpretation is deliberately platform-dependent, because file URIs are: the drive-letter rule applies only on Windows, since /C:/temp is a legal POSIX path naming a directory called C:. A URI naming a remote host is mapped to a UNC path on Windows and returned untouched elsewhere, so it fails visibly rather than being silently redirected to an unrelated local path (on Linux //host/share would otherwise resolve as /host/share). Any trailing separator is dropped so callers can uniformly append /<filename>.

FMI 3.0 is unaffected: it passes resourcePath as a plain path, and no Chrono FMI 3.0 component parses it.

Verification

New unit test utest_FMI_resource_path (9 cases) covering the conforming and Chrono-emitted forms, authority handling, percent decoding including UTF-8 and 0xFF escapes and malformed escapes, plain paths, platform-guarded drive-letter and remote-host behavior, locale independence, and malformed input. Passes on both Windows (clang 19 / MSVC headers) and Linux (gcc 13.3), where 8 pass and the locale case skips itself when no Turkish locale is installed.

End-to-end on Linux (Ubuntu 24.04, gcc 13.3), static build with FMU_EXPORT_SUPPORT=ON, FMU2cs_Powertrain built and loaded through fmi2Instantiate with a conforming three-slash URI:

WITHOUT this change:
   Engine JSON: tmp/ch-static/.../resources/EngineShafts.json
   ERROR: Could not open JSON file: tmp/ch-static/.../resources/EngineShafts.json
   Segmentation fault (core dumped)

WITH this change:
   Engine JSON: /tmp/ch-static/.../resources/EngineShafts.json
   RESULT: resources resolved, initialization completed

That reproduces the exact symptom reported in #762, including the segfault, and shows it resolved.

Test coverage notes

All six modified translation units compile on Windows, but the Windows FMU targets were not built locally, since FMU export requires a static build with a static MSVC runtime; CI covers that configuration.

Radu owns this code, so flagging for his review: cc @rserban

Fixes #762.

## Problem

The FMI 2.0 standard specifies that the `fmuResourceLocation` argument of
`fmi2Instantiate` is a URI (RFC 3986/8089) pointing to the `resources` directory
of the unpacked FMU, not a plain filesystem path. All six Chrono::Vehicle FMUs
converted it by removing a fixed-length prefix:

```cpp
auto resources_dir = std::string(fmuResourceLocation).erase(0, 8);
```

Chopping 8 characters removes `file:///`. On Windows that is correct, because the
drive letter follows the third slash and `file:///C:/temp` becomes `C:/temp`. On
POSIX it removes the leading slash of an absolute path, so
`file:///tmp/xyz/resources` becomes the relative `tmp/xyz/resources` and the JSON
specification files are not found.

## Why this went unnoticed

Chrono's own importer does not emit a conforming URI. fmu-forge builds the
location in `FmuUnit::Instantiate` as

```cpp
"file:///" + m_directory + "/resources"
```

and on POSIX `m_directory` is already absolute, so the result carries four
slashes (`file:////tmp/...`). A blind 8-character chop happens to land correctly
on that. A standards-conforming importer such as FMPy, Dymola or OpenModelica
emits three slashes, and the chop then eats the root slash. The practical effect
is that the Chrono vehicle FMUs work on Linux and macOS only with Chrono's own
loader, which defeats the purpose of shipping FMI components.

## Change

Replace the prefix removal at all six call sites with a shared
`chrono::fmi2::GetResourcesPath()`, in a new header with no fmu-forge dependency
so it can be included and tested on its own. `ChFmuToolsExport.h` includes it, so
the call sites change only in the call itself.

The parser handles an empty or `localhost` authority (matched case-insensitively,
since host names are), the Windows drive-letter form, the RFC 8089 minimal
`file:/path` form, repeated leading slashes so the existing four-slash form keeps
working, percent-encoded octets, and a plain path for importers that pass one
instead of a URI. Interpretation is deliberately platform-dependent, because file
URIs are: the drive-letter rule applies only on Windows, since `/C:/temp` is a
legal POSIX path naming a directory called `C:`. A URI naming a remote host is
mapped to a UNC path on Windows and returned untouched elsewhere, so it fails
visibly rather than being silently redirected to an unrelated local path (on
Linux `//host/share` would otherwise resolve as `/host/share`). Any trailing
separator is dropped so callers can uniformly append `/<filename>`.

FMI 3.0 is unaffected: it passes `resourcePath` as a plain path, and no Chrono
FMI 3.0 component parses it.

## Verification

New unit test `utest_FMI_resource_path` (9 cases) covering the conforming and
Chrono-emitted forms, authority handling, percent decoding including UTF-8 and
0xFF escapes and malformed escapes, plain paths, platform-guarded drive-letter
and remote-host behavior, locale independence, and malformed input. Passes on
both Windows (clang 19 / MSVC headers) and Linux (gcc 13.3), where 8 pass and the
locale case skips itself when no Turkish locale is installed.

End-to-end on Linux (Ubuntu 24.04, gcc 13.3), static build with
`FMU_EXPORT_SUPPORT=ON`, `FMU2cs_Powertrain` built and loaded through
`fmi2Instantiate` with a conforming three-slash URI:

```
WITHOUT this change:
   Engine JSON: tmp/ch-static/.../resources/EngineShafts.json
   ERROR: Could not open JSON file: tmp/ch-static/.../resources/EngineShafts.json
   Segmentation fault (core dumped)

WITH this change:
   Engine JSON: /tmp/ch-static/.../resources/EngineShafts.json
   RESULT: resources resolved, initialization completed
```

That reproduces the exact symptom reported in #762, including the segfault, and
shows it resolved.

## Test coverage notes

All six modified translation units compile on Windows, but the Windows FMU
targets were not built locally, since FMU export requires a static build with a
static MSVC runtime; CI covers that configuration.

Radu owns this code, so flagging for his review: cc @rserban

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DanNegrut
DanNegrut requested a review from rserban July 26, 2026 13:35
@DanNegrut

Copy link
Copy Markdown
Contributor Author

Follow-up verification.

The PR body reports the end-to-end check for FMU2cs_Powertrain only. Since opening it I have built and loaded all six Chrono::Vehicle FMUs on Linux (Ubuntu 24.04, gcc 13.3), static build with FMU_EXPORT_SUPPORT=ON.

Each FMU was exercised by dlopen-ing its .so and calling fmi2Instantiate, fmi2SetupExperiment and fmi2EnterInitializationMode directly, passing a spec-conforming three-slash file:///... resource URI. No external FMI importer is involved, so the check isolates exactly the URI-to-path conversion this PR changes.

All six resolve their resources with the leading slash intact:

FMU Resource(s) opened
FMU2cs_WheeledVehicle /…/resources/Vehicle.json
FMU2cs_WheeledVehiclePtrain Vehicle.json, EngineShafts.json, AutomaticTransmissionShafts.json
FMU2cs_Powertrain EngineShafts.json, AutomaticTransmissionShafts.json
FMU2cs_ForceElementTire TMeasyTire.json
FMU2cs_PathFollowerDriver ISO_double_lane_change.txt
FMU2me_PathFollowerDriver ISO_double_lane_change.txt

This includes FMU2cs_WheeledVehicle, which is the component in the original report: #762 shows Vehicle JSON: tmp/tmpyr1j7rvs/resources/Vehicle.json.

As a control, reverting just the changed line and rebuilding on the same machine reproduces the reported failure exactly, including the crash:

 Engine JSON: tmp/ch-static/.../resources/EngineShafts.json
 ERROR: Could not open JSON file: tmp/ch-static/.../resources/EngineShafts.json
 Segmentation fault (core dumped)

Not covered locally, left to CI: the Windows static-FMU configuration (FMU export requires a static build with a static MSVC runtime) and macOS.

One further note that is out of scope for this PR but worth recording. The reason this survived so long is that fmu-forge does not emit a conforming URI either: FmuUnit::Instantiate builds "file:///" + m_directory + "/resources", and on POSIX m_directory is already absolute, so Chrono's own importer produces a four-slash file:////tmp/.... The blind eight-character chop happened to land correctly on that, which is why Chrono-to-Chrono co-simulation always worked and only third-party importers broke. This PR keeps the four-slash form working, but the URI construction in fmu-forge is arguably the thing to fix upstream.

@rserban

rserban commented Jul 26, 2026

Copy link
Copy Markdown
Member

"This PR keeps the four-slash form working, but the URI construction in fmu-forge is arguably the thing to fix upstream."

This is exactly what I was planning on looking into. We own the fmu-forge code as well and so any fix should go in there (which may make Chrono changes in this PR unnecessary). Furthermore, fmu-forge was designed to also be used stand-alone (and indeed, some users do exactly that) and as such any issues should first be fixed there.

To summarize, I think the following should be done in this order:

  1. Implement and test any necessary fixes in fmu-forge.
  2. Test Chrono::FMI against a local clone of fmu-forge (the Chrono CMake configuration allows using an external fmu-forge code). Decide what (if anything) from this PR is still needed.
  3. Update the git submodule in Chrono to point to the latest commit in the fmu-forge repository.
  4. Test Chrono::FMI with the default fmu-forge from src/chrono_thirdparty.

@DanNegrut

DanNegrut commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

No argument on ownership, and agreed that a construction fix belongs in fmu-forge and needs to work
stand-alone. One thing I want to put on the record before step 2, because it decides whether that test
tells you the truth.

The fmu-forge construction failure is POSIX-only, and a Windows test of it will pass either way.

fmu-forge builds the URI by concatenation
(fmi2/FmuToolsImport.h:786, "file:///" + m_directory + "/resources"), so the number of slashes
depends on whether m_directory already starts with one:

m_directory constructed URI slashes old erase(0, 8) yields
/home/u/build/.../tmp_unpack (POSIX) file:////home/u/... 4 /home/u/... works
C:/Users/.../tmp_unpack (Windows) file:///C:/Users/... 3 C:/Users/... works

On Windows the current code already emits a spec-conforming three-slash URI and the old parser already
handles it, because what survives the chop is C:/..., which is absolute on its own. On POSIX the same
concatenation produces the non-conforming four-slash form, and the old parser only works because of
it.

So if step 2 is run on Windows, Chrono::FMI will pass with and without this PR, and the reasonable
conclusion would be that the PR is unnecessary. That conclusion would be wrong for POSIX. Worth running
step 2 on Linux specifically.

To be precise about scope: it is the fmu-forge round trip that is POSIX-only. Windows has a separate,
narrower problem of its own, in the third row of the table below, where a valid single-slash URI loses
its drive letter and yields a path that looks absolute and points somewhere else.

What the old parser does to conforming URIs, measured. "file:///" is exactly eight characters, so
erase(0, 8) is only correct for the four-slash form:

conforming input old parser outcome
file:///tmp/fmu/resources tmp/fmu/resources relative, this is #762
file:/tmp/fmu/resources p/fmu/resources eats into the path
file:/C:/temp/fmu/resources /temp/fmu/resources silently plausible and wrong, drive letter gone
file://localhost/tmp/fmu/resources ocalhost/tmp/fmu/resources mangled
file:///C:/temp/fmu/resources C:/temp/fmu/resources happens to work

The single-slash rows are worth a look independently of anything the standard does or does not require.
I have not gone back to the FMI 2.0 text myself, so I am not going to assert what it mandates. The
measurable point stands on its own: an importer that emits either single-slash form gets a mangled path
from Chrono today, and on Windows it gets one that looks correct and is not. So this is not only about
the importer we ship.

Correcting my own earlier framing. In the comment above I implied the reporter's trace identified
their importer. It does not. fmu-forge handed a relative unpack directory produces
file:///tmp/tmpyr1j7rvs/resources, and the old parser turns that into exactly the
tmp/tmpyr1j7rvs/resources they pasted. So the trace establishes that a three-slash URI reached the
parser, and nothing about which tool produced it. That does not change the conclusion, since either way
an FMU has to handle the conforming form, but the stronger claim was not mine to make.

On ordering. I am not suggesting your sequence breaks anything: the submodule bump is step 3, so
Chrono keeps the current fmu-forge until then and there is no window where the tree is broken. My only
suggestion is that the parsing fix is safe to land independently, because it accepts both the
conforming and the four-slash forms and so is a no-op against fmu-forge's present output on both
platforms. Landing it first would let you change the construction without having to preserve the
four-slash quirk for Chrono's benefit.

The comment introduced its whole list of accepted resource-location forms with "per RFC 8089",
but two of the entries are not standard file URIs and RFC 8089 does not describe them: the
four-slash form and a bare filesystem path. Only a later sentence noted the four-slash form
was non-standard, so the list as written implied the specification endorses both.

Split into two lists, standard and compatibility, with the non-standard status stated where
the forms are introduced rather than afterwards. No behavior change; comments only.

Also documents "file:/C:/temp/fmu/resources", the authority-omitted form with a drive letter,
which the parser already handled but the list did not mention. All seven documented rows were
checked against actual output before committing.

Adds one observation to the four-slash note: that form only arises where an absolute path
begins with a separator, so the same concatenation in our importer yields the standard
three-slash form on Windows, making the four-slash case POSIX-only. That asymmetry is the
reason a Windows-only test of this code path passes whether or not the parser is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] resources_dir path in linx so build missing '/' for absolut path

2 participants