Add support for LX symlinks (WSL/MSYS2 native symlinks)

- Add reparseTagLxSymlink constant (0xA000001D)
- Implement decode logic for LX symlinks (UTF-8 format)
- Add IsLxSymlink field to ReparsePoint struct to preserve symlink type
- Implement encode logic to recreate LX symlinks on import
- Add unit tests for LX symlink round-trip validation

Fixes issue where Docker builds with MSYS2 failed with 'unsupported reparse point a000001d' error.

Signed-off-by: Varun Gokulnath <vagokuln@microsoft.com>
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
This commit is contained in:
Varun Gokulnath
2025-12-17 11:16:26 -08:00
committed by Varun Gokulnath
parent 3c9576c934
commit 94113a48c2
2 changed files with 94 additions and 3 deletions
+34 -3
View File
@@ -15,6 +15,7 @@ import (
const (
reparseTagMountPoint = 0xA0000003
reparseTagSymlink = 0xA000000C
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
)
type reparseDataBuffer struct {
@@ -31,6 +32,7 @@ type reparseDataBuffer struct {
type ReparsePoint struct {
Target string
IsMountPoint bool
IsLxSymlink bool // True if this is an LX symlink (WSL/MSYS2 native)
}
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
@@ -56,6 +58,20 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
case reparseTagMountPoint:
isMountPoint = true
case reparseTagSymlink:
case reparseTagLxSymlink:
// LX symlinks store the target as UTF-8 after a 4-byte version field
if len(b) < 4 {
return nil, fmt.Errorf("LX symlink buffer too short")
}
targetBytes := b[4:]
for i, c := range targetBytes {
if c == 0 {
targetBytes = targetBytes[:i]
break
}
}
target := string(targetBytes)
return &ReparsePoint{Target: target, IsMountPoint: false, IsLxSymlink: true}, nil
default:
return nil, &UnsupportedReparsePointError{tag}
}
@@ -69,16 +85,31 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
if err != nil {
return nil, err
}
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint, false}, nil
}
func isDriveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
// mount point.
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink,
// mount point, or LX symlink.
func EncodeReparsePoint(rp *ReparsePoint) []byte {
if rp.IsLxSymlink {
// LX symlink: 4-byte version + UTF-8 target
version := uint32(2)
targetBytes := []byte(rp.Target)
dataLength := 4 + len(targetBytes)
var b bytes.Buffer
_ = binary.Write(&b, binary.LittleEndian, uint32(reparseTagLxSymlink))
_ = binary.Write(&b, binary.LittleEndian, uint16(dataLength))
_ = binary.Write(&b, binary.LittleEndian, uint16(0))
_ = binary.Write(&b, binary.LittleEndian, version)
_, _ = b.Write(targetBytes)
return b.Bytes()
}
// Generate an NT path and determine if this is a relative path.
var ntTarget string
relative := false
+60
View File
@@ -0,0 +1,60 @@
//go:build windows
// +build windows
package winio
import (
"testing"
)
func TestLxSymlinkRoundTrip(t *testing.T) {
// Test LX symlink encode/decode
original := &ReparsePoint{
Target: "/usr/bin/bash",
IsMountPoint: false,
IsLxSymlink: true,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify
if decoded.Target != original.Target {
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
}
if decoded.IsLxSymlink != original.IsLxSymlink {
t.Errorf("IsLxSymlink mismatch: got %v, want %v", decoded.IsLxSymlink, original.IsLxSymlink)
}
if decoded.IsMountPoint != original.IsMountPoint {
t.Errorf("IsMountPoint mismatch: got %v, want %v", decoded.IsMountPoint, original.IsMountPoint)
}
}
func TestWindowsSymlinkNotLx(t *testing.T) {
// Test that regular Windows symlinks are not marked as LX
original := &ReparsePoint{
Target: `C:\Windows\System32`,
IsMountPoint: false,
IsLxSymlink: false,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify it's NOT an LX symlink
if decoded.IsLxSymlink {
t.Errorf("Windows symlink incorrectly marked as LX symlink")
}
}