Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions reparse.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//go:build windows
// +build windows

package winio

import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"strings"
"unicode/utf16"
Expand All @@ -15,6 +15,7 @@ import (
const (
reparseTagMountPoint = 0xA0000003
reparseTagSymlink = 0xA000000C
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
)

type reparseDataBuffer struct {
Expand All @@ -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
Expand All @@ -51,14 +53,19 @@ func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
}

func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
isMountPoint := false
switch tag {
case reparseTagMountPoint:
isMountPoint = true
return decodeWindowsReparsePointData(b, true)
case reparseTagSymlink:
return decodeWindowsReparsePointData(b, false)
case reparseTagLxSymlink:
return decodeLxReparsePointData(b)
default:
return nil, &UnsupportedReparsePointError{tag}
}
}

func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
if !isMountPoint {
nameOffset += 4
Expand All @@ -69,16 +76,54 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
if err != nil {
return nil, err
}
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
return &ReparsePoint{Target: string(utf16.Decode(name)), IsMountPoint: isMountPoint, IsLxSymlink: false}, nil
}

func decodeLxReparsePointData(b []byte) (*ReparsePoint, error) {
// LX symlinks store the target as UTF-8 after a 4-byte version field
if len(b) < 4 {
return nil, errors.New("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
}

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 {
return encodeLxReparsePoint(rp)
}
return encodeWindowsReparsePoint(rp)
}

func encodeLxReparsePoint(rp *ReparsePoint) []byte {
// 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()
}

func encodeWindowsReparsePoint(rp *ReparsePoint) []byte {
// Generate an NT path and determine if this is a relative path.
var ntTarget string
relative := false
Expand Down
59 changes: 59 additions & 0 deletions reparse_lx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go: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")
}
}
Loading