Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
git-backup
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
3
Issues
3
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
git-backup
Commits
f3f694b9
Commit
f3f694b9
authored
Jun 13, 2018
by
Kirill Smelkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
~gofmt
parent
cc6ac54f
Changes
8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1843 additions
and
1844 deletions
+1843
-1844
git-backup.go
git-backup.go
+979
-980
git-backup_test.go
git-backup_test.go
+331
-331
git.go
git.go
+136
-136
gitobjects.go
gitobjects.go
+159
-159
set.go
set.go
+20
-20
sha1.go
sha1.go
+33
-33
util.go
util.go
+120
-120
util_test.go
util_test.go
+65
-65
No files found.
git-backup.go
View file @
f3f694b9
This diff is collapsed.
Click to expand it.
git-backup_test.go
View file @
f3f694b9
This diff is collapsed.
Click to expand it.
git.go
View file @
f3f694b9
...
...
@@ -21,142 +21,142 @@ package main
// Git-backup | Run git subprocess
import
(
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"lab.nexedi.com/kirr/go123/exc"
"lab.nexedi.com/kirr/go123/mem"
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"lab.nexedi.com/kirr/go123/exc"
"lab.nexedi.com/kirr/go123/mem"
)
// how/whether to redirect stdio of spawned process
type
StdioRedirect
int
const
(
PIPE
StdioRedirect
=
iota
// connect stdio channel via PIPE to parent (default value)
DontRedirect
PIPE
StdioRedirect
=
iota
// connect stdio channel via PIPE to parent (default value)
DontRedirect
)
type
RunWith
struct
{
stdin
string
stdout
StdioRedirect
// PIPE | DontRedirect
stderr
StdioRedirect
// PIPE | DontRedirect
raw
bool
// !raw -> stdout, stderr are stripped
env
map
[
string
]
string
// !nil -> subprocess environment setup from env
stdin
string
stdout
StdioRedirect
// PIPE | DontRedirect
stderr
StdioRedirect
// PIPE | DontRedirect
raw
bool
// !raw -> stdout, stderr are stripped
env
map
[
string
]
string
// !nil -> subprocess environment setup from env
}
// run `git *argv` -> error, stdout, stderr
func
_git
(
argv
[]
string
,
ctx
RunWith
)
(
err
error
,
stdout
,
stderr
string
)
{
debugf
(
"git %s"
,
strings
.
Join
(
argv
,
" "
))
cmd
:=
exec
.
Command
(
"git"
,
argv
...
)
stdoutBuf
:=
bytes
.
Buffer
{}
stderrBuf
:=
bytes
.
Buffer
{}
if
ctx
.
stdin
!=
""
{
cmd
.
Stdin
=
strings
.
NewReader
(
ctx
.
stdin
)
}
switch
ctx
.
stdout
{
case
PIPE
:
cmd
.
Stdout
=
&
stdoutBuf
case
DontRedirect
:
cmd
.
Stdout
=
os
.
Stdout
default
:
panic
(
"git: stdout redirect mode invalid"
)
}
switch
ctx
.
stderr
{
case
PIPE
:
cmd
.
Stderr
=
&
stderrBuf
case
DontRedirect
:
cmd
.
Stderr
=
os
.
Stderr
default
:
panic
(
"git: stderr redirect mode invalid"
)
}
if
ctx
.
env
!=
nil
{
env
:=
[]
string
{}
for
k
,
v
:=
range
ctx
.
env
{
env
=
append
(
env
,
k
+
"="
+
v
)
}
cmd
.
Env
=
env
}
err
=
cmd
.
Run
()
stdout
=
mem
.
String
(
stdoutBuf
.
Bytes
())
stderr
=
mem
.
String
(
stderrBuf
.
Bytes
())
if
!
ctx
.
raw
{
// prettify stdout (e.g. so that 'sha1\n' becomes 'sha1' and can be used directly
stdout
=
strings
.
TrimSpace
(
stdout
)
stderr
=
strings
.
TrimSpace
(
stderr
)
}
return
err
,
stdout
,
stderr
debugf
(
"git %s"
,
strings
.
Join
(
argv
,
" "
))
cmd
:=
exec
.
Command
(
"git"
,
argv
...
)
stdoutBuf
:=
bytes
.
Buffer
{}
stderrBuf
:=
bytes
.
Buffer
{}
if
ctx
.
stdin
!=
""
{
cmd
.
Stdin
=
strings
.
NewReader
(
ctx
.
stdin
)
}
switch
ctx
.
stdout
{
case
PIPE
:
cmd
.
Stdout
=
&
stdoutBuf
case
DontRedirect
:
cmd
.
Stdout
=
os
.
Stdout
default
:
panic
(
"git: stdout redirect mode invalid"
)
}
switch
ctx
.
stderr
{
case
PIPE
:
cmd
.
Stderr
=
&
stderrBuf
case
DontRedirect
:
cmd
.
Stderr
=
os
.
Stderr
default
:
panic
(
"git: stderr redirect mode invalid"
)
}
if
ctx
.
env
!=
nil
{
env
:=
[]
string
{}
for
k
,
v
:=
range
ctx
.
env
{
env
=
append
(
env
,
k
+
"="
+
v
)
}
cmd
.
Env
=
env
}
err
=
cmd
.
Run
()
stdout
=
mem
.
String
(
stdoutBuf
.
Bytes
())
stderr
=
mem
.
String
(
stderrBuf
.
Bytes
())
if
!
ctx
.
raw
{
// prettify stdout (e.g. so that 'sha1\n' becomes 'sha1' and can be used directly
stdout
=
strings
.
TrimSpace
(
stdout
)
stderr
=
strings
.
TrimSpace
(
stderr
)
}
return
err
,
stdout
,
stderr
}
// error a git command returned
type
GitError
struct
{
GitErrContext
*
exec
.
ExitError
GitErrContext
*
exec
.
ExitError
}
type
GitErrContext
struct
{
argv
[]
string
stdin
string
stdout
string
stderr
string
argv
[]
string
stdin
string
stdout
string
stderr
string
}
func
(
e
*
GitError
)
Error
()
string
{
msg
:=
e
.
GitErrContext
.
Error
()
if
e
.
stderr
==
""
{
msg
+=
"(failed)
\n
"
}
return
msg
msg
:=
e
.
GitErrContext
.
Error
()
if
e
.
stderr
==
""
{
msg
+=
"(failed)
\n
"
}
return
msg
}
func
(
e
*
GitErrContext
)
Error
()
string
{
msg
:=
"git "
+
strings
.
Join
(
e
.
argv
,
" "
)
if
e
.
stdin
==
""
{
msg
+=
" </dev/null
\n
"
}
else
{
msg
+=
" <<EOF
\n
"
+
e
.
stdin
if
!
strings
.
HasSuffix
(
msg
,
"
\n
"
)
{
msg
+=
"
\n
"
}
msg
+=
"EOF
\n
"
}
msg
+=
e
.
stderr
if
!
strings
.
HasSuffix
(
msg
,
"
\n
"
)
{
msg
+=
"
\n
"
}
return
msg
msg
:=
"git "
+
strings
.
Join
(
e
.
argv
,
" "
)
if
e
.
stdin
==
""
{
msg
+=
" </dev/null
\n
"
}
else
{
msg
+=
" <<EOF
\n
"
+
e
.
stdin
if
!
strings
.
HasSuffix
(
msg
,
"
\n
"
)
{
msg
+=
"
\n
"
}
msg
+=
"EOF
\n
"
}
msg
+=
e
.
stderr
if
!
strings
.
HasSuffix
(
msg
,
"
\n
"
)
{
msg
+=
"
\n
"
}
return
msg
}
// argv -> []string, ctx (for passing argv + RunWith handy - see ggit() for details)
func
_gitargv
(
argv
...
interface
{})
(
argvs
[]
string
,
ctx
RunWith
)
{
ctx_seen
:=
false
for
_
,
arg
:=
range
argv
{
switch
arg
:=
arg
.
(
type
)
{
case
string
:
argvs
=
append
(
argvs
,
arg
)
default
:
argvs
=
append
(
argvs
,
fmt
.
Sprint
(
arg
))
case
RunWith
:
if
ctx_seen
{
panic
(
"git: multiple RunWith contexts"
)
}
ctx
,
ctx_seen
=
arg
,
true
}
}
return
argvs
,
ctx
ctx_seen
:=
false
for
_
,
arg
:=
range
argv
{
switch
arg
:=
arg
.
(
type
)
{
case
string
:
argvs
=
append
(
argvs
,
arg
)
default
:
argvs
=
append
(
argvs
,
fmt
.
Sprint
(
arg
))
case
RunWith
:
if
ctx_seen
{
panic
(
"git: multiple RunWith contexts"
)
}
ctx
,
ctx_seen
=
arg
,
true
}
}
return
argvs
,
ctx
}
// run `git *argv` -> err, stdout, stderr
...
...
@@ -167,59 +167,59 @@ func _gitargv(argv ...interface{}) (argvs []string, ctx RunWith) {
//
// NOTE err is concrete *GitError, not error
func
ggit
(
argv
...
interface
{})
(
err
*
GitError
,
stdout
,
stderr
string
)
{
return
ggit2
(
_gitargv
(
argv
...
))
return
ggit2
(
_gitargv
(
argv
...
))
}
func
ggit2
(
argv
[]
string
,
ctx
RunWith
)
(
err
*
GitError
,
stdout
,
stderr
string
)
{
e
,
stdout
,
stderr
:=
_git
(
argv
,
ctx
)
eexec
,
_
:=
e
.
(
*
exec
.
ExitError
)
if
e
!=
nil
&&
eexec
==
nil
{
exc
.
Raisef
(
"git %s : "
,
strings
.
Join
(
argv
,
" "
),
e
)
}
if
eexec
!=
nil
{
err
=
&
GitError
{
GitErrContext
{
argv
,
ctx
.
stdin
,
stdout
,
stderr
},
eexec
}
}
return
err
,
stdout
,
stderr
e
,
stdout
,
stderr
:=
_git
(
argv
,
ctx
)
eexec
,
_
:=
e
.
(
*
exec
.
ExitError
)
if
e
!=
nil
&&
eexec
==
nil
{
exc
.
Raisef
(
"git %s : "
,
strings
.
Join
(
argv
,
" "
),
e
)
}
if
eexec
!=
nil
{
err
=
&
GitError
{
GitErrContext
{
argv
,
ctx
.
stdin
,
stdout
,
stderr
},
eexec
}
}
return
err
,
stdout
,
stderr
}
// run `git *argv` -> stdout
// on error - raise exception
func
xgit
(
argv
...
interface
{})
string
{
return
xgit2
(
_gitargv
(
argv
...
))
return
xgit2
(
_gitargv
(
argv
...
))
}
func
xgit2
(
argv
[]
string
,
ctx
RunWith
)
string
{
gerr
,
stdout
,
_
:=
ggit2
(
argv
,
ctx
)
if
gerr
!=
nil
{
exc
.
Raise
(
gerr
)
}
return
stdout
gerr
,
stdout
,
_
:=
ggit2
(
argv
,
ctx
)
if
gerr
!=
nil
{
exc
.
Raise
(
gerr
)
}
return
stdout
}
// like xgit(), but automatically parse stdout to Sha1
func
xgitSha1
(
argv
...
interface
{})
Sha1
{
return
xgit2Sha1
(
_gitargv
(
argv
...
))
return
xgit2Sha1
(
_gitargv
(
argv
...
))
}
// error when git output is not valid sha1
type
GitSha1Error
struct
{
GitErrContext
GitErrContext
}
func
(
e
*
GitSha1Error
)
Error
()
string
{
msg
:=
e
.
GitErrContext
.
Error
()
msg
+=
fmt
.
Sprintf
(
"expected valid sha1 (got %q)
\n
"
,
e
.
stdout
)
return
msg
msg
:=
e
.
GitErrContext
.
Error
()
msg
+=
fmt
.
Sprintf
(
"expected valid sha1 (got %q)
\n
"
,
e
.
stdout
)
return
msg
}
func
xgit2Sha1
(
argv
[]
string
,
ctx
RunWith
)
Sha1
{
gerr
,
stdout
,
stderr
:=
ggit2
(
argv
,
ctx
)
if
gerr
!=
nil
{
exc
.
Raise
(
gerr
)
}
sha1
,
err
:=
Sha1Parse
(
stdout
)
if
err
!=
nil
{
exc
.
Raise
(
&
GitSha1Error
{
GitErrContext
{
argv
,
ctx
.
stdin
,
stdout
,
stderr
}})
}
return
sha1
gerr
,
stdout
,
stderr
:=
ggit2
(
argv
,
ctx
)
if
gerr
!=
nil
{
exc
.
Raise
(
gerr
)
}
sha1
,
err
:=
Sha1Parse
(
stdout
)
if
err
!=
nil
{
exc
.
Raise
(
&
GitSha1Error
{
GitErrContext
{
argv
,
ctx
.
stdin
,
stdout
,
stderr
}})
}
return
sha1
}
gitobjects.go
View file @
f3f694b9
This diff is collapsed.
Click to expand it.
set.go
View file @
f3f694b9
...
...
@@ -25,44 +25,44 @@ package main
type
Sha1Set
map
[
Sha1
]
struct
{}
func
(
s
Sha1Set
)
Add
(
v
Sha1
)
{
s
[
v
]
=
struct
{}{}
s
[
v
]
=
struct
{}{}
}
func
(
s
Sha1Set
)
Contains
(
v
Sha1
)
bool
{
_
,
ok
:=
s
[
v
]
return
ok
_
,
ok
:=
s
[
v
]
return
ok
}
// all elements of set as slice
func
(
s
Sha1Set
)
Elements
()
[]
Sha1
{
ev
:=
make
([]
Sha1
,
len
(
s
))
i
:=
0
for
e
:=
range
s
{
ev
[
i
]
=
e
i
++
}
return
ev
ev
:=
make
([]
Sha1
,
len
(
s
))
i
:=
0
for
e
:=
range
s
{
ev
[
i
]
=
e
i
++
}
return
ev
}
// Set<string>
type
StrSet
map
[
string
]
struct
{}
func
(
s
StrSet
)
Add
(
v
string
)
{
s
[
v
]
=
struct
{}{}
s
[
v
]
=
struct
{}{}
}
func
(
s
StrSet
)
Contains
(
v
string
)
bool
{
_
,
ok
:=
s
[
v
]
return
ok
_
,
ok
:=
s
[
v
]
return
ok
}
// all elements of set as slice
func
(
s
StrSet
)
Elements
()
[]
string
{
ev
:=
make
([]
string
,
len
(
s
))
i
:=
0
for
e
:=
range
s
{
ev
[
i
]
=
e
i
++
}
return
ev
ev
:=
make
([]
string
,
len
(
s
))
i
:=
0
for
e
:=
range
s
{
ev
[
i
]
=
e
i
++
}
return
ev
}
sha1.go
View file @
f3f694b9
...
...
@@ -21,13 +21,13 @@ package main
// Git-backup | Sha1 type to work with SHA1 oids
import
(
"bytes"
"encoding/hex"
"fmt"
"bytes"
"encoding/hex"
"fmt"
"lab.nexedi.com/kirr/go123/mem"
"lab.nexedi.com/kirr/go123/mem"
git
"github.com/libgit2/git2go"
git
"github.com/libgit2/git2go"
)
const
SHA1_RAWSIZE
=
20
...
...
@@ -39,51 +39,51 @@ const SHA1_RAWSIZE = 20
// - slice size = 24 bytes
// -> so it is reasonable to pass Sha1 not by reference
type
Sha1
struct
{
sha1
[
SHA1_RAWSIZE
]
byte
sha1
[
SHA1_RAWSIZE
]
byte
}
// fmt.Stringer
var
_
fmt
.
Stringer
=
Sha1
{}
func
(
sha1
Sha1
)
String
()
string
{
return
hex
.
EncodeToString
(
sha1
.
sha1
[
:
])
return
hex
.
EncodeToString
(
sha1
.
sha1
[
:
])
}
func
Sha1Parse
(
sha1str
string
)
(
Sha1
,
error
)
{
sha1
:=
Sha1
{}
if
hex
.
DecodedLen
(
len
(
sha1str
))
!=
SHA1_RAWSIZE
{
return
Sha1
{},
fmt
.
Errorf
(
"sha1parse: %q invalid"
,
sha1str
)
}
_
,
err
:=
hex
.
Decode
(
sha1
.
sha1
[
:
],
mem
.
Bytes
(
sha1str
))
if
err
!=
nil
{
return
Sha1
{},
fmt
.
Errorf
(
"sha1parse: %q invalid: %s"
,
sha1str
,
err
)
}
return
sha1
,
nil
sha1
:=
Sha1
{}
if
hex
.
DecodedLen
(
len
(
sha1str
))
!=
SHA1_RAWSIZE
{
return
Sha1
{},
fmt
.
Errorf
(
"sha1parse: %q invalid"
,
sha1str
)
}
_
,
err
:=
hex
.
Decode
(
sha1
.
sha1
[
:
],
mem
.
Bytes
(
sha1str
))
if
err
!=
nil
{
return
Sha1
{},
fmt
.
Errorf
(
"sha1parse: %q invalid: %s"
,
sha1str
,
err
)
}
return
sha1
,
nil
}
// fmt.Scanner
var
_
fmt
.
Scanner
=
(
*
Sha1
)(
nil
)
func
(
sha1
*
Sha1
)
Scan
(
s
fmt
.
ScanState
,
ch
rune
)
error
{
switch
ch
{
case
's'
,
'v'
:
default
:
return
fmt
.
Errorf
(
"Sha1.Scan: invalid verb %q"
,
ch
)
}
tok
,
err
:=
s
.
Token
(
true
,
nil
)
if
err
!=
nil
{
return
err
}
*
sha1
,
err
=
Sha1Parse
(
mem
.
String
(
tok
))
return
err
switch
ch
{
case
's'
,
'v'
:
default
:
return
fmt
.
Errorf
(
"Sha1.Scan: invalid verb %q"
,
ch
)
}
tok
,
err
:=
s
.
Token
(
true
,
nil
)
if
err
!=
nil
{
return
err
}
*
sha1
,
err
=
Sha1Parse
(
mem
.
String
(
tok
))
return
err
}
// check whether sha1 is null
func
(
sha1
*
Sha1
)
IsNull
()
bool
{
return
*
sha1
==
Sha1
{}
return
*
sha1
==
Sha1
{}
}
// for sorting by Sha1
...
...
@@ -95,9 +95,9 @@ func (p BySha1) Less(i, j int) bool { return bytes.Compare(p[i].sha1[:], p[j].sh
// interoperability with git2go
func
(
sha1
*
Sha1
)
AsOid
()
*
git
.
Oid
{
return
(
*
git
.
Oid
)(
&
sha1
.
sha1
)
return
(
*
git
.
Oid
)(
&
sha1
.
sha1
)
}
func
Sha1FromOid
(
oid
*
git
.
Oid
)
Sha1
{
return
Sha1
{
*
oid
}
return
Sha1
{
*
oid
}
}
util.go
View file @
f3f694b9
...
...
@@ -21,161 +21,161 @@ package main
// Git-backup | Miscellaneous utilities
import
(
"encoding/hex"
"fmt"
"os"
"strings"
"syscall"
"unicode"
"unicode/utf8"
"lab.nexedi.com/kirr/go123/mem"
"encoding/hex"
"fmt"
"os"
"strings"
"syscall"
"unicode"
"unicode/utf8"
"lab.nexedi.com/kirr/go123/mem"
)
// strip_prefix("/a/b", "/a/b/c/d/e") -> "c/d/e" (without leading /)
// path must start with prefix
func
strip_prefix
(
prefix
,
path
string
)
string
{
if
!
strings
.
HasPrefix
(
path
,
prefix
)
{
panic
(
fmt
.
Errorf
(
"strip_prefix: %q has no prefix %q"
,
path
,
prefix
))
}
path
=
path
[
len
(
prefix
)
:
]
for
strings
.
HasPrefix
(
path
,
"/"
)
{
path
=
path
[
1
:
]
// strip leading /
}
return
path
if
!
strings
.
HasPrefix
(
path
,
prefix
)
{
panic
(
fmt
.
Errorf
(
"strip_prefix: %q has no prefix %q"
,
path
,
prefix
))
}
path
=
path
[
len
(
prefix
)
:
]
for
strings
.
HasPrefix
(
path
,
"/"
)
{
path
=
path
[
1
:
]
// strip leading /
}
return
path
}
// reprefix("/a", "/b", "/a/str") -> "/b/str"
// path must start with prefix_from
func
reprefix
(
prefix_from
,
prefix_to
,
path
string
)
string
{
path
=
strip_prefix
(
prefix_from
,
path
)
return
fmt
.
Sprintf
(
"%s/%s"
,
prefix_to
,
path
)
path
=
strip_prefix
(
prefix_from
,
path
)
return
fmt
.
Sprintf
(
"%s/%s"
,
prefix_to
,
path
)
}
// like ioutil.WriteFile() but takes native mode/perm
func
writefile
(
path
string
,
data
[]
byte
,
perm
uint32
)
error
{
fd
,
err
:=
syscall
.
Open
(
path
,
syscall
.
O_WRONLY
|
syscall
.
O_CREAT
|
syscall
.
O_TRUNC
,
perm
)
if
err
!=
nil
{
return
&
os
.
PathError
{
"open"
,
path
,
err
}
}
f
:=
os
.
NewFile
(
uintptr
(
fd
),
path
)
_
,
err
=
f
.
Write
(
data
)
err2
:=
f
.
Close
()
if
err
==
nil
{
err
=
err2
}
return
err
fd
,
err
:=
syscall
.
Open
(
path
,
syscall
.
O_WRONLY
|
syscall
.
O_CREAT
|
syscall
.
O_TRUNC
,
perm
)
if
err
!=
nil
{
return
&
os
.
PathError
{
"open"
,
path
,
err
}
}
f
:=
os
.
NewFile
(
uintptr
(
fd
),
path
)
_
,
err
=
f
.
Write
(
data
)
err2
:=
f
.
Close
()
if
err
==
nil
{
err
=
err2
}
return
err
}
// escape path so that git is happy to use it as ref
// https://git.kernel.org/cgit/git/git.git/tree/refs.c?h=v2.9.0-37-g6d523a3#n34
// XXX very suboptimal
func
path_refescape
(
path
string
)
string
{
outv
:=
[]
string
{}
for
_
,
component
:=
range
strings
.
Split
(
path
,
"/"
)
{
out
:=
""
dots
:=
0
// number of seen consecutive dots
for
len
(
component
)
>
0
{
r
,
size
:=
utf8
.
DecodeRuneInString
(
component
)
// no ".." anywhere - we replace dots run to %46%46... with trailing "."
// this way for single "." case we'll have it intact and avoid .. anywhere
// also this way: trailing .git is always encoded as ".git"
if
r
==
'.'
{
dots
+=
1
component
=
component
[
size
:
]
continue
}
if
dots
!=
0
{
out
+=
strings
.
Repeat
(
escape
(
"."
),
dots
-
1
)
out
+=
"."
dots
=
0
}
rbytes
:=
component
[
:
size
]
if
shouldEscape
(
r
)
{
rbytes
=
escape
(
rbytes
)
}
out
+=
rbytes
component
=
component
[
size
:
]
}
// handle trailing dots
if
dots
!=
0
{
out
+=
strings
.
Repeat
(
escape
(
"."
),
dots
-
1
)
out
+=
"."
}
if
len
(
out
)
>
0
{
// ^. not allowed
if
out
[
0
]
==
'.'
{
out
=
escape
(
"."
)
+
out
[
1
:
]
}
// .lock$ not allowed
if
strings
.
HasSuffix
(
out
,
".lock"
)
{
out
=
out
[
:
len
(
out
)
-
5
]
+
escape
(
"."
)
+
"lock"
}
}
outv
=
append
(
outv
,
out
)
}
// strip trailing /
for
len
(
outv
)
>
0
{
if
len
(
outv
[
len
(
outv
)
-
1
])
!=
0
{
break
}
outv
=
outv
[
:
len
(
outv
)
-
1
]
}
return
strings
.
Join
(
outv
,
"/"
)
outv
:=
[]
string
{}
for
_
,
component
:=
range
strings
.
Split
(
path
,
"/"
)
{
out
:=
""
dots
:=
0
// number of seen consecutive dots
for
len
(
component
)
>
0
{
r
,
size
:=
utf8
.
DecodeRuneInString
(
component
)
// no ".." anywhere - we replace dots run to %46%46... with trailing "."
// this way for single "." case we'll have it intact and avoid .. anywhere
// also this way: trailing .git is always encoded as ".git"
if
r
==
'.'
{
dots
+=
1
component
=
component
[
size
:
]
continue
}
if
dots
!=
0
{
out
+=
strings
.
Repeat
(
escape
(
"."
),
dots
-
1
)
out
+=
"."
dots
=
0
}
rbytes
:=
component
[
:
size
]
if
shouldEscape
(
r
)
{
rbytes
=
escape
(
rbytes
)
}
out
+=
rbytes
component
=
component
[
size
:
]
}
// handle trailing dots
if
dots
!=
0
{
out
+=
strings
.
Repeat
(
escape
(
"."
),
dots
-
1
)
out
+=
"."
}
if
len
(
out
)
>
0
{
// ^. not allowed
if
out
[
0
]
==
'.'
{
out
=
escape
(
"."
)
+
out
[
1
:
]
}
// .lock$ not allowed
if
strings
.
HasSuffix
(
out
,
".lock"
)
{
out
=
out
[
:
len
(
out
)
-
5
]
+
escape
(
"."
)
+
"lock"
}
}
outv
=
append
(
outv
,
out
)
}
// strip trailing /
for
len
(
outv
)
>
0
{
if
len
(
outv
[
len
(
outv
)
-
1
])
!=
0
{
break
}
outv
=
outv
[
:
len
(
outv
)
-
1
]
}
return
strings
.
Join
(
outv
,
"/"
)
}
func
shouldEscape
(
r
rune
)
bool
{
if
unicode
.
IsSpace
(
r
)
||
unicode
.
IsControl
(
r
)
{
return
true
}
switch
r
{
// NOTE RuneError is for always escaping non-valid UTF-8
case
':'
,
'?'
,
'['
,
'\\'
,
'^'
,
'~'
,
'*'
,
'@'
,
'%'
,
utf8
.
RuneError
:
return
true
}
return
false
if
unicode
.
IsSpace
(
r
)
||
unicode
.
IsControl
(
r
)
{
return
true
}
switch
r
{
// NOTE RuneError is for always escaping non-valid UTF-8
case
':'
,
'?'
,
'['
,
'\\'
,
'^'
,
'~'
,
'*'
,
'@'
,
'%'
,
utf8
.
RuneError
:
return
true
}
return
false
}
func
escape
(
s
string
)
string
{
out
:=
""
for
i
:=
0
;
i
<
len
(
s
);
i
++
{
out
+=
fmt
.
Sprintf
(
"%%%02X"
,
s
[
i
])
}
return
out
out
:=
""
for
i
:=
0
;
i
<
len
(
s
);
i
++
{
out
+=
fmt
.
Sprintf
(
"%%%02X"
,
s
[
i
])
}
return
out
}
// unescape path encoded by path_refescape()
// decoding is permissive - any byte can be %-encoded, not only special cases
// XXX very suboptimal
func
path_refunescape
(
s
string
)
(
string
,
error
)
{
l
:=
len
(
s
)
out
:=
make
([]
byte
,
0
,
len
(
s
))
for
i
:=
0
;
i
<
l
;
i
++
{
c
:=
s
[
i
]
if
c
==
'%'
{
if
i
+
2
>=
l
{
return
""
,
EscapeError
(
s
)
}
b
,
err
:=
hex
.
DecodeString
(
s
[
i
+
1
:
i
+
3
])
if
err
!=
nil
{
return
""
,
EscapeError
(
s
)
}
c
=
b
[
0
]
i
+=
2
}
out
=
append
(
out
,
c
)
}
return
mem
.
String
(
out
),
nil
l
:=
len
(
s
)
out
:=
make
([]
byte
,
0
,
len
(
s
))
for
i
:=
0
;
i
<
l
;
i
++
{
c
:=
s
[
i
]
if
c
==
'%'
{
if
i
+
2
>=
l
{
return
""
,
EscapeError
(
s
)
}
b
,
err
:=
hex
.
DecodeString
(
s
[
i
+
1
:
i
+
3
])
if
err
!=
nil
{
return
""
,
EscapeError
(
s
)
}
c
=
b
[
0
]
i
+=
2
}
out
=
append
(
out
,
c
)
}
return
mem
.
String
(
out
),
nil
}
type
EscapeError
string
func
(
e
EscapeError
)
Error
()
string
{
return
fmt
.
Sprintf
(
"%q: invalid escape format"
,
string
(
e
))
return
fmt
.
Sprintf
(
"%q: invalid escape format"
,
string
(
e
))
}
util_test.go
View file @
f3f694b9
...
...
@@ -20,81 +20,81 @@
package
main
import
(
"strings"
"testing"
"strings"
"testing"
)
func
TestPathEscapeUnescape
(
t
*
testing
.
T
)
{
type
TestEntry
struct
{
path
string
;
escapedv
[]
string
}
te
:=
func
(
path
string
,
escaped
...
string
)
TestEntry
{
return
TestEntry
{
path
,
escaped
}
}
var
tests
=
[]
TestEntry
{
// path escaped non-canonical escapes
te
(
"hello/world"
,
"hello/world"
,
"%68%65%6c%6c%6f%2f%77%6f%72%6c%64"
),
te
(
"hello/мир"
,
"hello/мир"
),
te
(
"hello/ мир"
,
"hello/%20мир"
),
te
(
"hel%lo/мир"
,
"hel%25lo/мир"
),
te
(
".hello/.world"
,
"%2Ehello/%2Eworld"
),
te
(
"..hello/world.loc"
,
"%2E.hello/world.loc"
),
te
(
"..hello/world.lock"
,
"%2E.hello/world%2Elock"
),
// leading /
te
(
"/hello/world"
,
"/hello/world"
),
te
(
"//hello///world"
,
"//hello///world"
),
// trailing /
te
(
"/hello/world/"
,
"/hello/world"
),
te
(
"/hello/world//"
,
"/hello/world"
),
type
TestEntry
struct
{
path
string
;
escapedv
[]
string
}
te
:=
func
(
path
string
,
escaped
...
string
)
TestEntry
{
return
TestEntry
{
path
,
escaped
}
}
var
tests
=
[]
TestEntry
{
// path escaped non-canonical escapes
te
(
"hello/world"
,
"hello/world"
,
"%68%65%6c%6c%6f%2f%77%6f%72%6c%64"
),
te
(
"hello/мир"
,
"hello/мир"
),
te
(
"hello/ мир"
,
"hello/%20мир"
),
te
(
"hel%lo/мир"
,
"hel%25lo/мир"
),
te
(
".hello/.world"
,
"%2Ehello/%2Eworld"
),
te
(
"..hello/world.loc"
,
"%2E.hello/world.loc"
),
te
(
"..hello/world.lock"
,
"%2E.hello/world%2Elock"
),
// leading /
te
(
"/hello/world"
,
"/hello/world"
),
te
(
"//hello///world"
,
"//hello///world"
),
// trailing /
te
(
"/hello/world/"
,
"/hello/world"
),
te
(
"/hello/world//"
,
"/hello/world"
),
// trailing ...
te
(
"/hello/world."
,
"/hello/world."
),
te
(
"/hello/world.."
,
"/hello/world%2E."
),
te
(
"/hello/world..."
,
"/hello/world%2E%2E."
),
te
(
"/hello/world...git"
,
"/hello/world%2E%2E.git"
),
// trailing ...
te
(
"/hello/world."
,
"/hello/world."
),
te
(
"/hello/world.."
,
"/hello/world%2E."
),
te
(
"/hello/world..."
,
"/hello/world%2E%2E."
),
te
(
"/hello/world...git"
,
"/hello/world%2E%2E.git"
),
// .. anywhere
te
(
"/hello/./world"
,
"/hello/%2E/world"
),
te
(
"/hello/.a/world"
,
"/hello/%2Ea/world"
),
te
(
"/hello/a./world"
,
"/hello/a./world"
),
te
(
"/hello/../world"
,
"/hello/%2E./world"
),
te
(
"/hello/a..b/world"
,
"/hello/a%2E.b/world"
),
te
(
"/hello/a.c.b/world"
,
"/hello/a.c.b/world"
),
te
(
"/hello/a.c..b/world"
,
"/hello/a.c%2E.b/world"
),
// .. anywhere
te
(
"/hello/./world"
,
"/hello/%2E/world"
),
te
(
"/hello/.a/world"
,
"/hello/%2Ea/world"
),
te
(
"/hello/a./world"
,
"/hello/a./world"
),
te
(
"/hello/../world"
,
"/hello/%2E./world"
),
te
(
"/hello/a..b/world"
,
"/hello/a%2E.b/world"
),
te
(
"/hello/a.c.b/world"
,
"/hello/a.c.b/world"
),
te
(
"/hello/a.c..b/world"
,
"/hello/a.c%2E.b/world"
),
// special & control characters
te
(
"/hel lo/wor
\t
ld/a:?[
\\
^~*@%b/
\0
01
\0
04
\n\xc2\xa0
"
,
"/hel%20lo/wor%09ld/a%3A%3F%5B%5C%5E%7E%2A%40%25b/%01%04%0A%C2%A0"
),
// special & control characters
te
(
"/hel lo/wor
\t
ld/a:?[
\\
^~*@%b/
\0
01
\0
04
\n\xc2\xa0
"
,
"/hel%20lo/wor%09ld/a%3A%3F%5B%5C%5E%7E%2A%40%25b/%01%04%0A%C2%A0"
),
// utf8 error
te
(
"a
\xc5
z"
,
"a%C5z"
),
}
// utf8 error
te
(
"a
\xc5
z"
,
"a%C5z"
),
}
for
_
,
tt
:=
range
tests
{
escaped
:=
path_refescape
(
tt
.
path
)
if
escaped
!=
tt
.
escapedv
[
0
]
{
t
.
Errorf
(
"path_refescape(%q) -> %q ; want %q"
,
tt
.
path
,
escaped
,
tt
.
escapedv
[
0
])
}
// also check the decoding
pathok
:=
strings
.
TrimRight
(
tt
.
path
,
"/"
)
for
_
,
escaped
:=
range
tt
.
escapedv
{
unescaped
,
err
:=
path_refunescape
(
escaped
)
if
unescaped
!=
pathok
||
err
!=
nil
{
t
.
Errorf
(
"path_refunescape(%q) -> %q %v ; want %q nil"
,
escaped
,
unescaped
,
err
,
tt
.
path
)
}
}
}
for
_
,
tt
:=
range
tests
{
escaped
:=
path_refescape
(
tt
.
path
)
if
escaped
!=
tt
.
escapedv
[
0
]
{
t
.
Errorf
(
"path_refescape(%q) -> %q ; want %q"
,
tt
.
path
,
escaped
,
tt
.
escapedv
[
0
])
}
// also check the decoding
pathok
:=
strings
.
TrimRight
(
tt
.
path
,
"/"
)
for
_
,
escaped
:=
range
tt
.
escapedv
{
unescaped
,
err
:=
path_refunescape
(
escaped
)
if
unescaped
!=
pathok
||
err
!=
nil
{
t
.
Errorf
(
"path_refunescape(%q) -> %q %v ; want %q nil"
,
escaped
,
unescaped
,
err
,
tt
.
path
)
}
}
}
}
func
TestPathUnescapeErr
(
t
*
testing
.
T
)
{
var
tests
=
[]
struct
{
escaped
string
}{
{
"%"
},
{
"%2"
},
{
"%2q"
},
{
"hell%2q/world"
},
}
var
tests
=
[]
struct
{
escaped
string
}{
{
"%"
},
{
"%2"
},
{
"%2q"
},
{
"hell%2q/world"
},
}
for
_
,
tt
:=
range
tests
{
unescaped
,
err
:=
path_refunescape
(
tt
.
escaped
)
if
err
==
nil
||
unescaped
!=
""
{
t
.
Errorf
(
"path_refunescape(%q) -> %q %v ; want
\"\"
err"
,
tt
.
escaped
,
unescaped
,
err
)
}
}
for
_
,
tt
:=
range
tests
{
unescaped
,
err
:=
path_refunescape
(
tt
.
escaped
)
if
err
==
nil
||
unescaped
!=
""
{
t
.
Errorf
(
"path_refunescape(%q) -> %q %v ; want
\"\"
err"
,
tt
.
escaped
,
unescaped
,
err
)
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment