Skip to content

Commit 1dafb01

Browse files
committed
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 <[email protected]>
1 parent 3c9576c commit 1dafb01

File tree

2 files changed

+94
-3
lines changed

2 files changed

+94
-3
lines changed

reparse.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
const (
1616
reparseTagMountPoint = 0xA0000003
1717
reparseTagSymlink = 0xA000000C
18+
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
1819
)
1920

2021
type reparseDataBuffer struct {
@@ -31,6 +32,7 @@ type reparseDataBuffer struct {
3132
type ReparsePoint struct {
3233
Target string
3334
IsMountPoint bool
35+
IsLxSymlink bool // True if this is an LX symlink (WSL/MSYS2 native)
3436
}
3537

3638
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
@@ -56,6 +58,20 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
5658
case reparseTagMountPoint:
5759
isMountPoint = true
5860
case reparseTagSymlink:
61+
case reparseTagLxSymlink:
62+
// LX symlinks store the target as UTF-8 after a 4-byte version field
63+
if len(b) < 4 {
64+
return nil, fmt.Errorf("LX symlink buffer too short")
65+
}
66+
targetBytes := b[4:]
67+
for i, c := range targetBytes {
68+
if c == 0 {
69+
targetBytes = targetBytes[:i]
70+
break
71+
}
72+
}
73+
target := string(targetBytes)
74+
return &ReparsePoint{Target: target, IsMountPoint: false, IsLxSymlink: true}, nil
5975
default:
6076
return nil, &UnsupportedReparsePointError{tag}
6177
}
@@ -69,16 +85,31 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
6985
if err != nil {
7086
return nil, err
7187
}
72-
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
88+
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint, false}, nil
7389
}
7490

7591
func isDriveLetter(c byte) bool {
7692
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
7793
}
7894

79-
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
80-
// mount point.
95+
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink,
96+
// mount point, or LX symlink.
8197
func EncodeReparsePoint(rp *ReparsePoint) []byte {
98+
if rp.IsLxSymlink {
99+
// LX symlink: 4-byte version + UTF-8 target
100+
version := uint32(2)
101+
targetBytes := []byte(rp.Target)
102+
dataLength := 4 + len(targetBytes)
103+
104+
var b bytes.Buffer
105+
_ = binary.Write(&b, binary.LittleEndian, uint32(reparseTagLxSymlink))
106+
_ = binary.Write(&b, binary.LittleEndian, uint16(dataLength))
107+
_ = binary.Write(&b, binary.LittleEndian, uint16(0))
108+
_ = binary.Write(&b, binary.LittleEndian, version)
109+
_, _ = b.Write(targetBytes)
110+
return b.Bytes()
111+
}
112+
82113
// Generate an NT path and determine if this is a relative path.
83114
var ntTarget string
84115
relative := false

reparse_lx_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package winio
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestLxSymlinkRoundTrip(t *testing.T) {
11+
// Test LX symlink encode/decode
12+
original := &ReparsePoint{
13+
Target: "/usr/bin/bash",
14+
IsMountPoint: false,
15+
IsLxSymlink: true,
16+
}
17+
18+
// Encode
19+
encoded := EncodeReparsePoint(original)
20+
21+
// Decode
22+
decoded, err := DecodeReparsePoint(encoded)
23+
if err != nil {
24+
t.Fatalf("Failed to decode: %v", err)
25+
}
26+
27+
// Verify
28+
if decoded.Target != original.Target {
29+
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
30+
}
31+
if decoded.IsLxSymlink != original.IsLxSymlink {
32+
t.Errorf("IsLxSymlink mismatch: got %v, want %v", decoded.IsLxSymlink, original.IsLxSymlink)
33+
}
34+
if decoded.IsMountPoint != original.IsMountPoint {
35+
t.Errorf("IsMountPoint mismatch: got %v, want %v", decoded.IsMountPoint, original.IsMountPoint)
36+
}
37+
}
38+
39+
func TestWindowsSymlinkNotLx(t *testing.T) {
40+
// Test that regular Windows symlinks are not marked as LX
41+
original := &ReparsePoint{
42+
Target: `C:\Windows\System32`,
43+
IsMountPoint: false,
44+
IsLxSymlink: false,
45+
}
46+
47+
// Encode
48+
encoded := EncodeReparsePoint(original)
49+
50+
// Decode
51+
decoded, err := DecodeReparsePoint(encoded)
52+
if err != nil {
53+
t.Fatalf("Failed to decode: %v", err)
54+
}
55+
56+
// Verify it's NOT an LX symlink
57+
if decoded.IsLxSymlink {
58+
t.Errorf("Windows symlink incorrectly marked as LX symlink")
59+
}
60+
}

0 commit comments

Comments
 (0)