1package winio
2
3import (
4	"bytes"
5	"encoding/binary"
6	"fmt"
7	"strings"
8	"unicode/utf16"
9	"unsafe"
10)
11
12const (
13	reparseTagMountPoint = 0xA0000003
14	reparseTagSymlink    = 0xA000000C
15)
16
17type reparseDataBuffer struct {
18	ReparseTag           uint32
19	ReparseDataLength    uint16
20	Reserved             uint16
21	SubstituteNameOffset uint16
22	SubstituteNameLength uint16
23	PrintNameOffset      uint16
24	PrintNameLength      uint16
25}
26
27// ReparsePoint describes a Win32 symlink or mount point.
28type ReparsePoint struct {
29	Target       string
30	IsMountPoint bool
31}
32
33// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
34// mount point reparse point.
35type UnsupportedReparsePointError struct {
36	Tag uint32
37}
38
39func (e *UnsupportedReparsePointError) Error() string {
40	return fmt.Sprintf("unsupported reparse point %x", e.Tag)
41}
42
43// DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink
44// or a mount point.
45func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
46	tag := binary.LittleEndian.Uint32(b[0:4])
47	return DecodeReparsePointData(tag, b[8:])
48}
49
50func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
51	isMountPoint := false
52	switch tag {
53	case reparseTagMountPoint:
54		isMountPoint = true
55	case reparseTagSymlink:
56	default:
57		return nil, &UnsupportedReparsePointError{tag}
58	}
59	nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
60	if !isMountPoint {
61		nameOffset += 4
62	}
63	nameLength := binary.LittleEndian.Uint16(b[6:8])
64	name := make([]uint16, nameLength/2)
65	err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name)
66	if err != nil {
67		return nil, err
68	}
69	return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
70}
71
72func isDriveLetter(c byte) bool {
73	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
74}
75
76// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
77// mount point.
78func EncodeReparsePoint(rp *ReparsePoint) []byte {
79	// Generate an NT path and determine if this is a relative path.
80	var ntTarget string
81	relative := false
82	if strings.HasPrefix(rp.Target, `\\?\`) {
83		ntTarget = `\??\` + rp.Target[4:]
84	} else if strings.HasPrefix(rp.Target, `\\`) {
85		ntTarget = `\??\UNC\` + rp.Target[2:]
86	} else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' {
87		ntTarget = `\??\` + rp.Target
88	} else {
89		ntTarget = rp.Target
90		relative = true
91	}
92
93	// The paths must be NUL-terminated even though they are counted strings.
94	target16 := utf16.Encode([]rune(rp.Target + "\x00"))
95	ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00"))
96
97	size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8
98	size += len(ntTarget16)*2 + len(target16)*2
99
100	tag := uint32(reparseTagMountPoint)
101	if !rp.IsMountPoint {
102		tag = reparseTagSymlink
103		size += 4 // Add room for symlink flags
104	}
105
106	data := reparseDataBuffer{
107		ReparseTag:           tag,
108		ReparseDataLength:    uint16(size),
109		SubstituteNameOffset: 0,
110		SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2),
111		PrintNameOffset:      uint16(len(ntTarget16) * 2),
112		PrintNameLength:      uint16((len(target16) - 1) * 2),
113	}
114
115	var b bytes.Buffer
116	binary.Write(&b, binary.LittleEndian, &data)
117	if !rp.IsMountPoint {
118		flags := uint32(0)
119		if relative {
120			flags |= 1
121		}
122		binary.Write(&b, binary.LittleEndian, flags)
123	}
124
125	binary.Write(&b, binary.LittleEndian, ntTarget16)
126	binary.Write(&b, binary.LittleEndian, target16)
127	return b.Bytes()
128}
129