1// Copyright 2021 Keybase Inc. All rights reserved.
2// Use of this source code is governed by a BSD
3// license that can be found in the LICENSE file.
4
5package libkb
6
7import (
8	"fmt"
9	"strings"
10)
11
12const escapeSacrificeForWindows = '‰'
13
14const disallowedRunesOnWindows = "<>:\"/\\|?*"
15
16var kbfsNameToWindowsReplaceSequence [][2]string
17var windowsNameToKbfsReplaceSequence [][2]string
18
19func init() {
20	makeEscapePair := func(r rune) [2]string {
21		return [2]string{string(r), fmt.Sprintf("‰%x", r)}
22	}
23	makeUnescapePairs := func(r rune) [][2]string {
24		lower := fmt.Sprintf("‰%x", r)
25		upper := fmt.Sprintf("‰%X", r)
26		if lower == upper {
27			return [][2]string{
28				{lower, string(r)},
29			}
30		}
31		return [][2]string{
32			{lower, string(r)},
33			{upper, string(r)},
34		}
35	}
36
37	kbfsNameToWindowsReplaceSequence = nil
38	windowsNameToKbfsReplaceSequence = nil
39
40	kbfsNameToWindowsReplaceSequence = append(kbfsNameToWindowsReplaceSequence,
41		makeEscapePair(escapeSacrificeForWindows),
42	)
43	for _, r := range disallowedRunesOnWindows {
44		kbfsNameToWindowsReplaceSequence = append(
45			kbfsNameToWindowsReplaceSequence, makeEscapePair(r))
46		windowsNameToKbfsReplaceSequence = append(
47			windowsNameToKbfsReplaceSequence, makeUnescapePairs(r)...)
48	}
49	windowsNameToKbfsReplaceSequence = append(windowsNameToKbfsReplaceSequence,
50		makeUnescapePairs(escapeSacrificeForWindows)...)
51}
52
53// EncodeKbfsNameForWindows encodes a KBFS  path element for Windows by
54// escaping disallowed characters.
55func EncodeKbfsNameForWindows(kbfsName string) (windowsName string) {
56	// fast path for names that don't have characters that need escaping
57	if !strings.ContainsAny(kbfsName, disallowedRunesOnWindows) &&
58		!strings.ContainsRune(kbfsName, escapeSacrificeForWindows) {
59		return kbfsName
60	}
61	windowsName = kbfsName
62	for _, replacement := range kbfsNameToWindowsReplaceSequence {
63		windowsName = strings.ReplaceAll(windowsName, replacement[0], replacement[1])
64	}
65	return windowsName
66}
67
68// InvalidWindowsNameError is the error returned when an invalid path name is
69// passed in.
70type InvalidWindowsNameError struct{}
71
72// Error implements the error interface.
73func (InvalidWindowsNameError) Error() string {
74	return "invalid windows path name"
75}
76
77// DecodeWindowsNameForKbfs decodes a path element encoded by
78// EncodeKbfsNameForWindows.
79func DecodeWindowsNameForKbfs(windowsName string) (kbfsName string, err error) {
80	if strings.ContainsAny(windowsName, disallowedRunesOnWindows) {
81		return "", InvalidWindowsNameError{}
82	}
83
84	// fast path for names that don't have escaped characters
85	if !strings.ContainsRune(windowsName, escapeSacrificeForWindows) {
86		return windowsName, nil
87	}
88
89	kbfsName = windowsName
90	for _, replacement := range windowsNameToKbfsReplaceSequence {
91		kbfsName = strings.ReplaceAll(kbfsName, replacement[0], replacement[1])
92	}
93	return kbfsName, nil
94}
95