Bug #11867
Covnersion fails if the abl folder is symlinked
100%
Related issues
History
#1 Updated by Alexandru Lungu about 1 month ago
- Assignee set to Dănuț Filimon
ConversionDriver crashes with a NullPointerException during incremental conversion whenever the file-set's <directory> reference (basepath, e.g. "./abl/") is a symlink pointing to a directory whose own final path component has a different name than the symlink itself.
- In the hotel_gui project, move the real source content aside and replace it with a symlink under a different target name, e.g.:
- mv abl abl_src
- ln -s abl_src abl (abl_src can be any name that differs from "abl" - that mismatch is what triggers the bug.)
- Confirm cfg/p2j.cfg.xml's profile has a file-set directory reference for this path, e.g.:
- <file-set>
- <directory recursive="true" path="./abl/" spec="(.[pPwWtT]|.cls)" />
- Run the conversion driver in incremental mode against that profile.
Expected is that the files under abl/ (via the symlink) are found and conversion proceeds normally, same as when abl is a real directory. But it fails with:
java.lang.NullPointerException: Cannot invoke
"com.goldencode.artifacts.ArtifactCollection.getArtifacts()" because
"col" is null
at com.goldencode.artifacts.ArtifactCollection.<init>(ArtifactCollection.java:112)
at com.goldencode.p2j.convert.TransformDriver.executeJob(TransformDriver.java:1149)
at com.goldencode.p2j.convert.ConversionDriver.main(ConversionDriver.java:1458)
Root cause:
FileListFactory.resolvePaths() (FileListFactory.java:429-447) reconstructs each matched file's relative path from a real (symlink-resolved) absolute path using segment-count arithmetic:
Path real = path.toRealPath();
Path base = real.subpath(0, real.getNameCount() - path.getNameCount());
Path resolved = real.subpath(base.getNameCount(), real.getNameCount());
This is only correct when the symlink's target ends in a path component named identically to the symlink itself. Whenever it differs (the normal case for any real symlink), the computed "resolved" path is wrong and does not exist relative to the project root. ExplicitFileList.listImpl() then silently drops every such entry because it only keeps files that actually exist on disk, so the whole file-set resolves to zero files.
That empty result reaches ArtifactManager.getArtifactCol(), whose "if (col.isEmpty()) return null;" turns the empty file-set into a null collection instead of an empty one; ArtifactManager.createProjectArtifacts() (PROFILE case) then wraps that null in a "success" ArtifactConversionResult (code=1), so job.artifacts ends up null. In incremental mode, TransformDriver.executeJob()'s "new ArtifactCollection(job.artifacts)" then throws the NPE above.
Suggested fix:
FileListFactory.resolvePaths() should not reconstruct the relative path from segment counts at all - it already has the original, normalized relative path and only needs toRealPath() to confirm the file exists; it should return the original path string rather than a path derived from the real (symlink-resolved) segments.
#2 Updated by Dănuț Filimon about 1 month ago
- Related to Bug #11156: Conversion fails if source folder is a symlink added
#3 Updated by Dănuț Filimon about 1 month ago
- Status changed from New to WIP
Looks like the same problem from #11156, I will rebase the branch and retest.
#4 Updated by Dănuț Filimon 30 days ago
- Status changed from WIP to Review
- % Done changed from 0 to 100
I committed 11156a/16754, the symbolic link will not be followed in resolvePaths().
Alexandru, please review.
#5 Updated by Dănuț Filimon 30 days ago
- reviewer Alexandru Lungu added
#6 Updated by Alexandru Lungu 5 days ago
Danut, I looked at the changes in 11156a. They look right to me. But I wonder one thing: doesn't it affect the portability? In other words, I see many absolutePath being used, but if these paths reach some artifacts (including H2 incremental conversion database), won't this mean that the project should be converted always in the same place.
- As a regression test, can you convert a project at a patch, move the project somewhere else and run incremental conversion?
- Also think of OO and other more peculiar cases where conversion paths may end up being saved in artifacts / H2 database.
- Also consider Windows system for regression testing these changes.
- Nevertheless, mind that conversion is also being used at run-time, so double-check that there is no security backdoor into these changes done now (like converting a query that does some
..,/etclogic).
AI review:
- [MAJOR] functional
SymbolResolver.exists: The exact-case branch now returnsfile.getAbsolutePath()(symlink preserved), but the case-insensitive fallback still callsFileSystemDaemon.getCaseInsensitiveMatch(file, true), which canonicalizes both input and result and therefore resolves symlinks.pkgExistsalways callsexists(file, false)for PROPATH entries (4GL class matching is case-insensitive), so on Linux any case-mismatched package reference reaches the fallback regardless of thecase-sensitivesetting. With a symlinked package directory under basepath (e.g.abl/lib -> /elsewhere/shared),USING lib.util.*normalizes to./lib/util, whileUSING Lib.Util.*yields the canonical/elsewhere/shared/util.normalizeFilenamecannot relativize that path, even with its canonical fallback, so the USING handling writesnormalized-path=.elsewhere.shared.util.oo_references.rulesthen drops the Java import or builds a wrong package name. Trunk was uniformly canonical, so this branch leaves the result dependent on the letter case of the reference, which defeats its symlink-preserving goal. The case-insensitive path needs the same non-canonical treatment: match against the absolute/normalized path and returngetAbsolutePath(). - [MAJOR] functional
FileScope.resolveFileName: On a Windows conversion host,getCanonicalPath()used to fold a path to its on-disk case.getAbsolutePath()/Path.normalize()now keep the case as referenced. Because NTFS is case-insensitive, two references differing only in case (e.g.{Inc/X.i}and{inc/x.i}) both pass the firstcanRead(), andfindCaseInsensitivelyis never reached.openthen setsfileName = normalizeFilename(fullname), producing./Inc/X.iand./inc/x.i. SinceArtifactKeycompares names with plainString.equals/hashCode, one file becomes two artifacts. This splits pphints, include dependency tracking and AST registration. The same applies to class/package paths fromSymbolResolver.exists, to the AST artifact names inAstGenerator, and to the case of the&{FILE_NAME}output. Linux hosts are unaffected. Windows conversion is explicitly supported (CallGraphHelper.isWindowsOs). - [MINOR] functional
ArtifactManager.normalizeFilename:..segments are now collapsed lexically byPath.normalize(), while the file itself is still opened through the raw path, where the OS resolves..physically. Example: propath entry./abl/app, whereabl/appis a symlink to/x/app, and an include{../common/x.i}.FileScope.resolveFileNamebuilds/proj/./abl/app/../common/x.i, andopenreads/x/common/x.i. The registered key, however, becomes./abl/common/x.i, which lies under home, so the canonical fallback never runs. That key feeds theClearStreamIncludeHintartifact,&{FILE_NAME}, theCallGraphWorkerfindUniqueNodelookup (which creates a phantom node for a non-existent path) andloadIncludeHints, all of which point at a missing or different file. Trunk's realpath kept the key consistent with the file actually read.
Style review is omitted for brevity. Regenerate the review yourself to capture the styling issues after we finish the functional bullets.