Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c60ccc233 | ||
|
|
3175093adf | ||
|
|
72a9609acb | ||
|
|
b2923eaa4a | ||
|
|
c3218a122f | ||
|
|
65bb5c6302 | ||
|
|
0cacad260e | ||
|
|
40f02fba65 | ||
|
|
e5bb87aaa1 | ||
|
|
80b84ad6b9 | ||
|
|
395537a5d2 | ||
|
|
dc03b85283 | ||
|
|
56aec4c702 | ||
|
|
951128c735 | ||
|
|
8e4187fe19 | ||
|
|
403a2ea84b | ||
|
|
7f4444b8a3 | ||
|
|
bd7666b3e6 | ||
|
|
876b8cffc4 | ||
|
|
6bfa914a51 | ||
|
|
24ef16e80e | ||
|
|
cc6b6987f8 | ||
|
|
1b217ca440 | ||
|
|
7587b8067a | ||
|
|
2b7fb84a5e | ||
|
|
963f2ddb32 | ||
|
|
3ff2620ba5 | ||
|
|
22c83d8f2d | ||
|
|
cab407a098 | ||
|
|
4d21cd5dd8 | ||
|
|
26cb73de1c | ||
|
|
caef8c0fbd | ||
|
|
ea3d3fdcef | ||
|
|
4d2c5587d6 | ||
|
|
6b3f0f0ae6 | ||
|
|
28e9a2c0ca | ||
|
|
e5677bec86 | ||
|
|
a1b16115df | ||
|
|
5439149b09 | ||
|
|
332276f6db | ||
|
|
66c45e1c6e | ||
|
|
68654f8bd3 | ||
|
|
a95e38802d | ||
|
|
e063187656 | ||
|
|
892546343a | ||
|
|
dc3b5695d4 | ||
|
|
f8ee0c0459 | ||
|
|
947bbe9efa | ||
|
|
9af2b5b5f6 | ||
|
|
7e1387a787 | ||
|
|
ff1f38c494 | ||
|
|
de1d2ba243 | ||
|
|
2ab2c553b6 | ||
|
|
6889309af7 | ||
|
|
61758ec3ce | ||
|
|
7a1831c6ec | ||
|
|
31cafdfd16 | ||
|
|
859574f1e0 | ||
|
|
b4efbd1b6b | ||
|
|
a7fc7054c5 | ||
|
|
9dd2d6da30 | ||
|
|
ad85705599 | ||
|
|
b94703f6b9 | ||
|
|
acad061682 | ||
|
|
d89a841bfb | ||
|
|
b6a2908b68 | ||
|
|
37b71cb7bf | ||
|
|
644cbbcfd6 | ||
|
|
f9b2518734 |
3
.gitignore
vendored
@@ -23,8 +23,7 @@ _testmain.go
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
dist/
|
||||
|
||||
gohttpserver
|
||||
bindata_assetfs.go
|
||||
|
||||
assets_vfsdata.go
|
||||
|
||||
44
.goreleaser.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
project_name: gohttpserver
|
||||
release:
|
||||
github:
|
||||
owner: codeskyblue
|
||||
name: gohttpserver
|
||||
brew:
|
||||
github:
|
||||
owner: codeskyblue
|
||||
name: homebrew-tap
|
||||
homepage: https://github.com/codeskyblue/gohttpserver
|
||||
builds:
|
||||
- goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
goarm:
|
||||
- "6"
|
||||
main: .
|
||||
ldflags: -s -w -X main.VERSION={{.Version}}
|
||||
flags: -tags vfs
|
||||
binary: gohttpserver
|
||||
hooks:
|
||||
pre: go generate .
|
||||
archive:
|
||||
format: zip
|
||||
name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{
|
||||
.Arm }}{{ end }}'
|
||||
files:
|
||||
- licence*
|
||||
- LICENCE*
|
||||
- license*
|
||||
- LICENSE*
|
||||
- readme*
|
||||
- README*
|
||||
- changelog*
|
||||
- CHANGELOG*
|
||||
- .ghs.yml
|
||||
snapshot:
|
||||
name_template: SNAPSHOT-{{ .Commit }}
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
|
||||
13
.travis.yml
@@ -1,9 +1,12 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.5
|
||||
- 1.6
|
||||
env:
|
||||
global:
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
- "1.10"
|
||||
script:
|
||||
- go test -v
|
||||
deploy:
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: curl -sL https://git.io/goreleaser | bash
|
||||
on:
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME = linux
|
||||
|
||||
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM golang:1.10
|
||||
WORKDIR /go/src/github.com/codeskyblue/gohttpserver
|
||||
ADD . /go/src/github.com/codeskyblue/gohttpserver/
|
||||
RUN go get -v
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o gohttpserver .
|
||||
|
||||
FROM debian:stretch
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/public
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
VOLUME /app/public
|
||||
ADD assets ./assets
|
||||
COPY --from=0 /go/src/github.com/codeskyblue/gohttpserver/gohttpserver .
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT [ "/app/gohttpserver" ]
|
||||
CMD ["--root=/app/public"]
|
||||
59
Godeps/Godeps.json
generated
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/codeskyblue/gohttpserver",
|
||||
"GoVersion": "go1.6",
|
||||
"GodepVersion": "v74",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/DHowett/go-plist",
|
||||
"Rev": "f4bf55d2395500aacc17eedcebc5f139336b4312"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/kingpin",
|
||||
"Comment": "v2.1.3",
|
||||
"Rev": "aef28d186e59d39ed537473dfce4472108ea1045"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/template",
|
||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/template/parse",
|
||||
"Rev": "b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alecthomas/units",
|
||||
"Rev": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/codeskyblue/dockerignore",
|
||||
"Rev": "de82dee623d9207f906d327172149cba50427a88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-yaml/yaml",
|
||||
"Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/goji/httpauth",
|
||||
"Rev": "2da839ab0f4df05a6db5eb277995589dadbd4fb9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/context",
|
||||
"Comment": "v1.1-4-gaed02d1",
|
||||
"Rev": "aed02d124ae4a0e94fea4541c8effd05bf0c8296"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/handlers",
|
||||
"Comment": "v1.1-10-g801d6e3",
|
||||
"Rev": "801d6e3b008914ee888c9ab9b1b379b9a56fbf44"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.1-15-gd391bea",
|
||||
"Rev": "d391bea3118c9fc17a88d62c9189bb791255e0ef"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mash/go-accesslog",
|
||||
"Rev": "9ba8e13f36087d6cb83d9a9f17f9e8da137d5ee9"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
Godeps/Readme
generated
@@ -1,5 +0,0 @@
|
||||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 shengxiang
|
||||
Copyright (c) 2018 shengxiang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
176
README.md
@@ -1,19 +1,18 @@
|
||||
# gohttpserver
|
||||
[](https://travis-ci.org/codeskyblue/gohttpserver)
|
||||
|
||||
Make the best HTTP File Server. Better UI, upload support, apple&android install package qrcode generate.
|
||||
- Goal: Make the best HTTP File Server.
|
||||
- Features: Human-friendly UI, file uploading support, direct QR-code generation for Apple & Android install package.
|
||||
|
||||
[Demo site](https://gohttpserver.herokuapp.com/)
|
||||
|
||||
- 目标: 做最好的HTTP文件服务器
|
||||
- 功能: 人性化的UI体验,文件的上传支持,安卓和苹果安装包的二维码直接生成。
|
||||
|
||||
**Binary** can be download from [github releases](https://github.com/codeskyblue/gohttpserver/releases/)
|
||||
**Binaries** can be downloaded from [this repo releases](https://github.com/codeskyblue/gohttpserver/releases/)
|
||||
|
||||
## Notes
|
||||
If using go1.5, ensure you set GO15VENDOREXPERIMENT=1
|
||||
|
||||
Upload size now limited to 1G
|
||||
## Requirements
|
||||
Tested with go-1.10, go-1.11
|
||||
|
||||
## Screenshots
|
||||

|
||||
@@ -24,7 +23,7 @@ Upload size now limited to 1G
|
||||
1. [x] All assets package to Standalone binary
|
||||
1. [x] Different file type different icon
|
||||
1. [x] Support show or hide hidden files
|
||||
1. [x] Upload support (for security reason, you need enabled it by option `--upload`)
|
||||
1. [x] Upload support (auth by token or session)
|
||||
1. [x] README.md preview
|
||||
1. [x] HTTP Basic Auth
|
||||
1. [x] Partial reload pages when directory change
|
||||
@@ -47,9 +46,14 @@ Upload size now limited to 1G
|
||||
1. [ ] Support sort by size or modified time
|
||||
1. [x] Add version info into index page
|
||||
1. [ ] Add api `/-/info/some.(apk|ipa)` to get detail info
|
||||
1. [x] Add api `/-/apk/info/some.apk` to get android package info
|
||||
1. [x] Auto tag version
|
||||
1. [x] Custom title support
|
||||
1. [x] Support setting from conf file
|
||||
1. [x] Quick copy download link
|
||||
1. [x] Show folder size
|
||||
1. [x] Create folder
|
||||
1. [x] Skip delete confirm when alt pressed
|
||||
|
||||
## Installation
|
||||
```
|
||||
@@ -58,22 +62,84 @@ cd $GOPATH/src/github.com/codeskyblue/gohttpserver
|
||||
go build && ./gohttpserver
|
||||
```
|
||||
|
||||
Or download binaries from [github releases](https://github.com/codeskyblue/gohttpserver/releases)
|
||||
|
||||
## Usage
|
||||
Listen port 8000 on all interface, and enable upload
|
||||
Listen on port 8000 of all interfaces, and enable file uploading.
|
||||
|
||||
```
|
||||
./gohttpserver -r ./ --addr :8000 --upload
|
||||
./gohttpserver -r ./ --port 8000 --upload
|
||||
```
|
||||
|
||||
Use command `gohttpserver --help` to see more usage.
|
||||
|
||||
## Docker Usage
|
||||
share current directory
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver codeskyblue/gohttpserver
|
||||
```
|
||||
|
||||
Share current directory with http basic auth
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
codeskyblue/gohttpserver --root /app/public \
|
||||
--auth-type http --auth-http username:password
|
||||
```
|
||||
|
||||
Share current directory with openid auth. (Works only in netease company.)
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8000:8000 -v $PWD:/app/public --name gohttpserver \
|
||||
codeskyblue/gohttpserver --root /app/public \
|
||||
--auth-type openid
|
||||
```
|
||||
|
||||
## Authentication options
|
||||
- Enable basic http authentication
|
||||
|
||||
```sh
|
||||
$ gohttpserver --auth-type http --auth-http username:password
|
||||
```
|
||||
|
||||
- Use openid auth
|
||||
|
||||
```sh
|
||||
$ gohttpserver --auth-type openid --auth-openid https://login.example-hostname.com/openid/
|
||||
```
|
||||
|
||||
- Enable upload
|
||||
|
||||
```sh
|
||||
$ gohttpserver --upload
|
||||
```
|
||||
|
||||
- Enable delete and Create folder
|
||||
|
||||
```sh
|
||||
$ gohttpserver --delete
|
||||
```
|
||||
|
||||
## Advanced usage
|
||||
Support update access rule if there is a file named `.ghs.yml` under directory. `.ghs.yml` example
|
||||
Add access rule by creating a `.ghs.yml` file under a sub-directory. An example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
upload: false
|
||||
delete: false
|
||||
users:
|
||||
- email: "codeskyblue@codeskyblue.com"
|
||||
delete: true
|
||||
upload: true
|
||||
token: 4567gf8asydhf293r23r
|
||||
```
|
||||
|
||||
For example, if there is such file under directory `foo`, directory `foo` can not be uploaded, while `bar` can.
|
||||
In this case, if openid auth is enabled and user "codeskyblue@codeskyblue.com" has logged in, he/she can delete/upload files under the directory where the `.ghs.yml` file exits.
|
||||
|
||||
`token` is used for upload. see [upload with curl](#upload-with-curl)
|
||||
|
||||
For example, in the following directory hierarchy, users can delete/uploade files in directory `foo`, but he/she cannot do this in directory `bar`.
|
||||
|
||||
```
|
||||
root -
|
||||
@@ -84,62 +150,97 @@ root -
|
||||
`-- hello.txt
|
||||
```
|
||||
|
||||
Use config file. specfied with `--conf`, see [example config.yml](testdata/config.yml). Note that command line option can overwrite conf in `config.yml`
|
||||
User can specify config file name with `--conf`, see [example config.yml](testdata/config.yml).
|
||||
|
||||
To specify which files is hidden and which file is visible, add the following lines to `.ghs.yml`
|
||||
|
||||
```yaml
|
||||
accessTables:
|
||||
- regex: block.file
|
||||
allow: false
|
||||
- regex: visual.file
|
||||
allow: true
|
||||
```
|
||||
|
||||
### ipa plist proxy
|
||||
This is used for server which not https enabled. default use <https://plistproxy.herokuapp.com/plist>
|
||||
This is used for server on which https is enabled. default use <https://plistproxy.herokuapp.com/plist>
|
||||
|
||||
```
|
||||
./gohttpserver --plistproxy=https://someproxyhost.com/
|
||||
```
|
||||
|
||||
Proxy web site should have ability
|
||||
Test if proxy works:
|
||||
|
||||
```sh
|
||||
$ http POST https://proxyhost.com/plist < app.plist
|
||||
$ http POST https://someproxyhost.com/plist < app.plist
|
||||
{
|
||||
"key": "18f99211"
|
||||
}
|
||||
$ http GET https://proxyhost.com/plist/18f99211
|
||||
$ http GET https://someproxyhost.com/plist/18f99211
|
||||
# show the app.plist content
|
||||
```
|
||||
|
||||
### Upload with CURL
|
||||
For example, upload a file named `foo.txt` to directory `somedir`
|
||||
|
||||
PS: max upload size limited to 1G (hard coded)
|
||||
|
||||
```sh
|
||||
$ curl -F file=@foo.txt localhost:8000/somedir
|
||||
{"destination":"somedir/foo.txt","success":true}
|
||||
# upload with token
|
||||
$ curl -F file=@foo.txt -F token=12312jlkjafs localhost:8000/somedir
|
||||
{"destination":"somedir/foo.txt","success":true}
|
||||
```
|
||||
|
||||
### Deploy with nginx
|
||||
Recommended configuration, assume your gohttpserver listening on `127.0.0.1:8200`
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain-name.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8200; # here need to change
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
client_max_body_size 0; # disable upload limit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Refs: <http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>
|
||||
|
||||
## FAQ
|
||||
- [How to generate self signed certificate with openssl](http://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl)
|
||||
|
||||
### How the search works
|
||||
The search algorithm follow the search engine google. keywords are seperated with space, words with prefix `-` will be excluded.
|
||||
### How the query is formated
|
||||
The search query follows common format rules just like Google. Keywords are seperated with space(s), keywords with prefix `-` will be excluded in search results.
|
||||
|
||||
1. `hello world` means must contains `hello` and `world`
|
||||
1. `hello -world` means must contains `hello` but not contains `world`
|
||||
|
||||
## Developer Guide
|
||||
Depdencies are managed by godep
|
||||
Depdencies are managed by [govendor](https://github.com/kardianos/govendor)
|
||||
|
||||
```sh
|
||||
go get -v github.com/tools/godep
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
go get github.com/elazarl/go-bindata-assetfs/...
|
||||
```
|
||||
1. Build develop version. **assets** directory must exists
|
||||
|
||||
Theme are all defined in [res/themes](res/themes) directory. Now only two, black and green.
|
||||
```sh
|
||||
go build
|
||||
./gohttpserver
|
||||
```
|
||||
2. Build single binary release
|
||||
|
||||
## How to build single binary release
|
||||
```sh
|
||||
go-bindata-assetfs -tags bindata res/...
|
||||
go build -tags bindata
|
||||
```
|
||||
```sh
|
||||
go generate .
|
||||
go build -tags vfs
|
||||
```
|
||||
|
||||
Theme are defined in [assets/themes](assets/themes) directory. Now only two themes are available, "black" and "green".
|
||||
|
||||
That's all. ^_^
|
||||
|
||||
## Reference Web sites
|
||||
|
||||
@@ -150,14 +251,17 @@ That's all. ^_^
|
||||
* Markdown CSS <https://github.com/sindresorhus/github-markdown-css>
|
||||
* Upload support <http://www.dropzonejs.com/>
|
||||
* ScrollUp <https://markgoodyear.com/2013/01/scrollup-jquery-plugin/>
|
||||
* Clipboard <https://clipboardjs.com/>
|
||||
* Underscore <http://underscorejs.org/>
|
||||
|
||||
**Go Libraries**
|
||||
|
||||
* <https://github.com/elazarl/go-bindata-assetfs>
|
||||
* [vfsgen](https://github.com/shurcooL/vfsgen)
|
||||
* [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) Not using now
|
||||
* <http://www.gorillatoolkit.org/pkg/handlers>
|
||||
|
||||
## History
|
||||
The first version is <https://github.com/codeskyblue/gohttp>
|
||||
The old version is hosted at <https://github.com/codeskyblue/gohttp>
|
||||
|
||||
## LICENSE
|
||||
This project is under license [MIT](LICENSE)
|
||||
This project is licensed under [MIT](LICENSE).
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
@@ -24,3 +24,11 @@ div.dropzone {
|
||||
.qrcode-title {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.clearfix::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#qrcodeCanvas {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
275
assets/index.html
Normal file
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>gohttp server</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/-/assets/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/bootstrap-3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/font-awesome-4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/css/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/css/scrollUp-image.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/assets/themes/[[.Theme]].css">
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">[[.Title]]</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="hidden-xs">
|
||||
<a href="javascript:void(0)" v-on:click='genQrcode()'>
|
||||
View in Phone
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
</li>
|
||||
[[if eq .AuthType "openid"]]
|
||||
<template v-if="!user.email">
|
||||
<a href="/-/login" class="btn btn-sm btn-default navbar-btn">
|
||||
Sign in <span class="glyphicon glyphicon-user"></span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a href="/-/logout" class="btn btn-sm btn-default navbar-btn">
|
||||
<span v-text="user.name"></span>
|
||||
<i class="fa fa-sign-out"></i>
|
||||
</a>
|
||||
</template>
|
||||
[[end]]
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right">
|
||||
<div class="input-group">
|
||||
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search"
|
||||
autofocus>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
|
||||
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
|
||||
</li>
|
||||
<li v-if="breadcrumb.length >= 1">
|
||||
{{breadcrumb.slice(-1)[0].name}}
|
||||
</li>
|
||||
</ol>
|
||||
<table class="table table-hover" v-if="!previewMode">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan=4>
|
||||
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button> -->
|
||||
<div>
|
||||
<button class="btn btn-xs btn-default" onclick="history.back()">
|
||||
Back <i class="fa fa-arrow-left"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-show="auth.upload" data-toggle="modal" data-target="#upload-modal">
|
||||
Upload <i class="fa fa-upload"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-show="auth.delete" @click="makeDirectory">
|
||||
New Folder <i class="fa fa-folder"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th class="hidden-xs">
|
||||
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in computedFiles">
|
||||
<td>
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path + (f.type == 'dir' ? '' : '')}}">
|
||||
<!-- ?raw=false -->
|
||||
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td><span v-if="f.type == 'dir'">~</span> {{f.size | formatBytes}}</td>
|
||||
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
|
||||
<td style="text-align: left">
|
||||
<template v-if="f.type == 'dir'">
|
||||
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
|
||||
<span class="hidden-xs">Archive</span> Zip
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
|
||||
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="f.type == 'file'">
|
||||
<a class="btn btn-default btn-xs hidden-xs" href="/{{f.path}}?download=true">
|
||||
<span class="hidden-xs">Download</span>
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs bstooltip" data-trigger="manual" data-title="Copied!"
|
||||
data-clipboard-text="{{genDownloadURL(f)}}">
|
||||
<i class="fa fa-copy"></i>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs" v-on:click="showInfo(f)">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
</button>
|
||||
<button class="btn btn-default btn-xs hidden-xs" v-on:click="genQrcode(f.name)">
|
||||
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</button>
|
||||
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
|
||||
Install <i class="fa fa-cube"></i>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)">
|
||||
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
|
||||
</button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-12" id="preview" v-if="preview.filename">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-weight: normal">
|
||||
<i class="fa" v-bind:class='genFileClass(previewFile)'></i> {{preview.filename}}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<article class="markdown-body">{{{preview.contentHTML }}}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" id="content">
|
||||
<!-- Small qrcode modal -->
|
||||
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="qrcode-title"></span>
|
||||
<a style="font-size: 0.6em" href="#" id="qrcode-link">[view]</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body clearfix">
|
||||
<div id="qrcodeCanvas" class="pull-left"></div>
|
||||
<div id="qrcodeRight" class="pull-left">
|
||||
<p>
|
||||
<a href="#">下载链接</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload modal-->
|
||||
<div id="upload-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-upload"></i> File upload
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="#" class="dropzone" id="upload-form"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" @click="removeAllUploads">RemoveAll</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- File info modal -->
|
||||
<div id="file-info-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="file-info-title"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="file-info-content"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
||||
<a href="https://github.com/codeskyblue/gohttpserver">gohttpserver (ver:{{version}})</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>.
|
||||
Copyright 2016-2018. go1.10
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/-/assets/js/jquery-3.1.0.min.js"></script>
|
||||
<script src="/-/assets/js/jquery.qrcode.js"></script>
|
||||
<script src="/-/assets/js/jquery.scrollUp.min.js"></script>
|
||||
<script src="/-/assets/js/qrcode.js"></script>
|
||||
<script src="/-/assets/js/vue-1.0.min.js"></script>
|
||||
<script src="/-/assets/js/showdown-1.6.4.min.js"></script>
|
||||
<script src="/-/assets/js/moment.min.js"></script>
|
||||
<script src="/-/assets/js/dropzone.js"></script>
|
||||
<script src="/-/assets/js/underscore-min.js"></script>
|
||||
<script src="/-/assets/js/clipboard-1.5.12.min.js"></script>
|
||||
<script src="/-/assets/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src='/-/assets/[["js/index.js" | urlhash ]]'></script>
|
||||
<!-- <script src="/-/assets/js/index.js"></script> -->
|
||||
[[if .GoogleTrackerID ]]
|
||||
<script>
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '[[.GoogleTrackerID]]', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script> [[ end ]]
|
||||
</body>
|
||||
|
||||
</html>
|
||||
71
assets/ipa-install.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>[[.Name]] install</title>
|
||||
<meta http-equiv="Content-Type" content="text/HTML; charset=utf-8">
|
||||
<meta content="target-densitydpi=device-dpi,width=640" name="viewport" id="viewport">
|
||||
<link rel="shortcut icon" type="image/png" href="/-/assets/favicon.png" />
|
||||
<script type="text/javascript" src="/-/assets/js/ua-parser.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function showById(name) {
|
||||
document.getElementById(name).style.display = 'block';
|
||||
}
|
||||
|
||||
function checkBrowerAndDownload() {
|
||||
var parser = new UAParser();
|
||||
var os_info = parser.getOS();
|
||||
console.log(os_info)
|
||||
|
||||
var plistLink = "[[.PlistLink]]";
|
||||
var ipaInstallLink = 'itms-services://?action=download-manifest&url=' + plistLink;
|
||||
document.getElementById('itms-link').href = ipaInstallLink;
|
||||
|
||||
// wechat is support AppStore link now.
|
||||
if (navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == "micromessenger") {
|
||||
showById('safari');
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else if (os_info.name == 'Android') {
|
||||
showById("android");
|
||||
return;
|
||||
} else if (os_info.name == 'iOS') {
|
||||
showById('safari');
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else {
|
||||
showById('browser');
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
#wechat {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
min-height: 777px;
|
||||
}
|
||||
</style>
|
||||
<div id="wechat" style="display: none">
|
||||
<img style='width: 100%;position: relative;' src='/-/assets/imgs/wx.png' />
|
||||
</div>
|
||||
<div id="browser" style="display: none">
|
||||
This is IPA install page, you should open this link with your iPhone.
|
||||
</div>
|
||||
<div id="safari" style="display: none">
|
||||
If install not started soon, click <a id="itms-link" href="#">here</a>
|
||||
</div>
|
||||
<div id="android" style="display: none">
|
||||
This is IPA install page, not for android.
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
checkBrowerAndDownload();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
7
assets/js/clipboard-1.5.12.min.js
vendored
Normal file
406
assets/js/index.js
Normal file
@@ -0,0 +1,406 @@
|
||||
jQuery('#qrcodeCanvas').qrcode({
|
||||
text: "http://jetienne.com/"
|
||||
});
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
function getExtention(fname) {
|
||||
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
|
||||
}
|
||||
|
||||
function pathJoin(parts, sep) {
|
||||
var separator = sep || '/';
|
||||
var replace = new RegExp(separator + '{1,}', 'g');
|
||||
return parts.join(separator).replace(replace, separator);
|
||||
}
|
||||
|
||||
function getQueryString(name) {
|
||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
var r = decodeURI(window.location.search).substr(1).match(reg);
|
||||
if (r != null) return r[2].replace(/\+/g, ' ');
|
||||
return null;
|
||||
}
|
||||
|
||||
var vm = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
user: {
|
||||
email: "",
|
||||
name: "",
|
||||
},
|
||||
location: window.location,
|
||||
breadcrumb: [],
|
||||
showHidden: false,
|
||||
previewMode: false,
|
||||
preview: {
|
||||
filename: '',
|
||||
filetype: '',
|
||||
filesize: 0,
|
||||
contentHTML: '',
|
||||
},
|
||||
version: "loading",
|
||||
mtimeTypeFromNow: false, // or fromNow
|
||||
auth: {},
|
||||
search: getQueryString("search"),
|
||||
files: [{
|
||||
name: "loading ...",
|
||||
path: "",
|
||||
size: "...",
|
||||
type: "dir",
|
||||
}],
|
||||
myDropzone: null,
|
||||
},
|
||||
computed: {
|
||||
computedFiles: function () {
|
||||
var that = this;
|
||||
that.preview.filename = null;
|
||||
|
||||
var files = this.files.filter(function (f) {
|
||||
if (f.name == 'README.md') {
|
||||
that.preview.filename = f.name;
|
||||
}
|
||||
if (!that.showHidden && f.name.slice(0, 1) === '.') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// console.log(this.previewFile)
|
||||
if (this.preview.filename) {
|
||||
var name = this.preview.filename; // For now only README.md
|
||||
console.log(pathJoin([location.pathname, 'README.md']))
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, 'README.md']),
|
||||
method: 'GET',
|
||||
success: function (res) {
|
||||
var converter = new showdown.Converter({
|
||||
tables: true,
|
||||
omitExtraWLInCodeBlocks: true,
|
||||
parseImgDimensions: true,
|
||||
simplifiedAutoLink: true,
|
||||
literalMidWordUnderscores: true,
|
||||
tasklists: true,
|
||||
ghCodeBlocks: true,
|
||||
smoothLivePreview: true,
|
||||
simplifiedAutoLink: true,
|
||||
strikethrough: true,
|
||||
});
|
||||
|
||||
var html = converter.makeHtml(res);
|
||||
that.preview.contentHTML = html;
|
||||
},
|
||||
error: function (err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return files;
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
$.ajax({
|
||||
url: "/-/user",
|
||||
method: "get",
|
||||
dataType: "json",
|
||||
success: function (ret) {
|
||||
if (ret) {
|
||||
this.user.email = ret.email;
|
||||
this.user.name = ret.name;
|
||||
}
|
||||
}.bind(this)
|
||||
})
|
||||
this.myDropzone = new Dropzone("#upload-form", {
|
||||
paramName: "file",
|
||||
maxFilesize: 10240,
|
||||
addRemoveLinks: true,
|
||||
init: function () {
|
||||
this.on("uploadprogress", function (file, progress) {
|
||||
// console.log("File progress", progress);
|
||||
});
|
||||
this.on("complete", function (file) {
|
||||
console.log("reload file list")
|
||||
loadFileList()
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
formatTime: function (timestamp) {
|
||||
var m = moment(timestamp);
|
||||
if (this.mtimeTypeFromNow) {
|
||||
return m.fromNow();
|
||||
}
|
||||
return m.format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
toggleHidden: function () {
|
||||
this.showHidden = !this.showHidden;
|
||||
},
|
||||
removeAllUploads: function () {
|
||||
this.myDropzone.removeAllFiles();
|
||||
},
|
||||
genInstallURL: function (name, noEncode) {
|
||||
var parts = [location.host];
|
||||
if (!name) {
|
||||
parts.push(location.pathname);
|
||||
} else if (getExtention(name) == "ipa") {
|
||||
parts.push("/-/ipa/link", location.pathname, name);
|
||||
} else {
|
||||
parts.push(location.pathname, name);
|
||||
}
|
||||
var urlPath = location.protocol + "//" + pathJoin(parts);
|
||||
return noEncode ? urlPath : encodeURI(urlPath);
|
||||
},
|
||||
genQrcode: function (name, title) {
|
||||
var urlPath = this.genInstallURL(name, true);
|
||||
$("#qrcode-title").html(title || name || location.pathname);
|
||||
$("#qrcode-link").attr("href", urlPath);
|
||||
$('#qrcodeCanvas').empty().qrcode({
|
||||
text: urlPath
|
||||
});
|
||||
|
||||
$("#qrcodeRight a").attr("href", encodeURI(urlPath));
|
||||
$("#qrcode-modal").modal("show");
|
||||
},
|
||||
genDownloadURL: function (f) {
|
||||
return location.origin + "/" + f.path;
|
||||
},
|
||||
shouldHaveQrcode: function (name) {
|
||||
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
|
||||
},
|
||||
genFileClass: function (f) {
|
||||
if (f.type == "dir") {
|
||||
if (f.name == '.git') {
|
||||
return 'fa-git-square';
|
||||
}
|
||||
return "fa-folder-open";
|
||||
}
|
||||
var ext = getExtention(f.name);
|
||||
switch (ext) {
|
||||
case "go":
|
||||
case "py":
|
||||
case "js":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "fa-file-code-o";
|
||||
case "pdf":
|
||||
return "fa-file-pdf-o";
|
||||
case "zip":
|
||||
return "fa-file-zip-o";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
return "fa-file-audio-o";
|
||||
case "jpg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "jpeg":
|
||||
case "tiff":
|
||||
return "fa-file-picture-o";
|
||||
case "ipa":
|
||||
case "dmg":
|
||||
return "fa-apple";
|
||||
case "apk":
|
||||
return "fa-android";
|
||||
case "exe":
|
||||
return "fa-windows";
|
||||
}
|
||||
return "fa-file-text-o"
|
||||
},
|
||||
clickFileOrDir: function (f, e) {
|
||||
// TODO: fix here tomorrow
|
||||
if (f.type == "file") {
|
||||
return true;
|
||||
}
|
||||
var reqPath = pathJoin([location.pathname, f.name]);
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
changePath: function (reqPath, e) {
|
||||
loadFileOrDir(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
showInfo: function (f) {
|
||||
console.log(f);
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/info", location.pathname, f.name]),
|
||||
method: "GET",
|
||||
success: function (res) {
|
||||
$("#file-info-title").text(f.name);
|
||||
$("#file-info-content").text(JSON.stringify(res, null, 4));
|
||||
$("#file-info-modal").modal("show");
|
||||
// console.log(JSON.stringify(res, null, 4));
|
||||
}
|
||||
})
|
||||
},
|
||||
makeDirectory: function () {
|
||||
var name = window.prompt("Directory name?")
|
||||
console.log(name)
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/mkdir", location.pathname]),
|
||||
data: {
|
||||
name: name,
|
||||
},
|
||||
method: "POST",
|
||||
success: function (res) {
|
||||
console.log(res)
|
||||
loadFileList()
|
||||
},
|
||||
error: function (err) {
|
||||
alert(err.responseText);
|
||||
}
|
||||
})
|
||||
},
|
||||
deletePathConfirm: function (f, e) {
|
||||
e.preventDefault();
|
||||
if (!e.altKey) { // skip confirm when alt pressed
|
||||
if (!window.confirm("Delete " + f.name + " ?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, f.name]),
|
||||
method: 'DELETE',
|
||||
success: function (res) {
|
||||
loadFileList()
|
||||
},
|
||||
error: function (err) {
|
||||
alert(err.responseText);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateBreadcrumb: function () {
|
||||
var pathname = decodeURI(location.pathname || "/");
|
||||
var parts = pathname.split('/');
|
||||
this.breadcrumb = [];
|
||||
if (pathname == "/") {
|
||||
return this.breadcrumb;
|
||||
}
|
||||
var i = 2;
|
||||
for (; i <= parts.length; i += 1) {
|
||||
var name = parts[i - 1];
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
var path = parts.slice(0, i).join('/');
|
||||
this.breadcrumb.push({
|
||||
name: name + (i == parts.length ? ' /' : ''),
|
||||
path: path
|
||||
})
|
||||
}
|
||||
return this.breadcrumb;
|
||||
},
|
||||
loadPreviewFile: function (filepath, e) {
|
||||
if (e) {
|
||||
e.preventDefault() // may be need a switch
|
||||
}
|
||||
var that = this;
|
||||
$.getJSON(pathJoin(['/-/info', location.pathname]))
|
||||
.then(function (res) {
|
||||
console.log(res);
|
||||
that.preview.filename = res.name;
|
||||
that.preview.filesize = res.size;
|
||||
return $.ajax({
|
||||
url: '/' + res.path,
|
||||
dataType: 'text',
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
console.log(res)
|
||||
that.preview.contentHTML = '<pre>' + res + '</pre>';
|
||||
console.log("Finally")
|
||||
})
|
||||
.done(function (res) {
|
||||
console.log("done", res)
|
||||
});
|
||||
},
|
||||
loadAll: function () {
|
||||
// TODO: move loadFileList here
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
window.onpopstate = function (event) {
|
||||
var pathname = decodeURI(location.pathname)
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
function loadFileOrDir(reqPath) {
|
||||
window.history.pushState({}, "", reqPath);
|
||||
loadFileList(reqPath)
|
||||
}
|
||||
|
||||
function loadFileList(pathname) {
|
||||
var pathname = pathname || location.pathname;
|
||||
// console.log("load filelist:", pathname)
|
||||
if (getQueryString("raw") !== "false") { // not a file preview
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/json", pathname]),
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
success: function (res) {
|
||||
res.files = _.sortBy(res.files, function (f) {
|
||||
var weight = f.type == 'dir' ? 1000 : 1;
|
||||
return -weight * f.mtime;
|
||||
})
|
||||
|
||||
vm.files = res.files;
|
||||
vm.auth = res.auth;
|
||||
},
|
||||
error: function (err) {
|
||||
console.error(err)
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
vm.updateBreadcrumb();
|
||||
vm.previewMode = getQueryString("raw") == "false";
|
||||
if (vm.previewMode) {
|
||||
vm.loadPreviewFile();
|
||||
}
|
||||
}
|
||||
|
||||
Vue.filter('fromNow', function (value) {
|
||||
return moment(value).fromNow();
|
||||
})
|
||||
|
||||
Vue.filter('formatBytes', function (value) {
|
||||
var bytes = parseFloat(value);
|
||||
if (bytes < 0) return "-";
|
||||
else if (bytes < 1024) return bytes + " B";
|
||||
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
|
||||
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
|
||||
else return (bytes / 1073741824).toFixed(1) + " GB";
|
||||
})
|
||||
|
||||
$(function () {
|
||||
$.scrollUp({
|
||||
scrollText: '', // text are defined in css
|
||||
});
|
||||
|
||||
// For page first loading
|
||||
loadFileList(location.pathname + location.search)
|
||||
|
||||
// update version
|
||||
$.getJSON("/-/sysinfo", function (res) {
|
||||
vm.version = res.version;
|
||||
})
|
||||
|
||||
var clipboard = new Clipboard('.btn');
|
||||
clipboard.on('success', function (e) {
|
||||
console.info('Action:', e.action);
|
||||
console.info('Text:', e.text);
|
||||
console.info('Trigger:', e.trigger);
|
||||
$(e.trigger)
|
||||
.tooltip('show')
|
||||
.mouseleave(function () {
|
||||
$(this).tooltip('hide');
|
||||
})
|
||||
|
||||
e.clearSelection();
|
||||
});
|
||||
});
|
||||
5
assets/js/showdown-1.6.4.min.js
vendored
Normal file
9
assets_dev.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build !vfs
|
||||
//go:generate go run assets_generate.go
|
||||
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Assets contains project assets.
|
||||
var Assets http.FileSystem = http.Dir("assets")
|
||||
23
assets_generate.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var fs http.FileSystem = http.Dir("assets")
|
||||
|
||||
err := vfsgen.Generate(fs, vfsgen.Options{
|
||||
PackageName: "main",
|
||||
BuildTags: "vfs",
|
||||
VariableName: "Assets",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -15,10 +17,24 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"regexp"
|
||||
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/shogo82148/androidbinary/apk"
|
||||
)
|
||||
|
||||
const YAMLCONF = ".ghs.yml"
|
||||
|
||||
type ApkInfo struct {
|
||||
PackageName string `json:"packageName"`
|
||||
MainActivity string `json:"mainActivity"`
|
||||
Version struct {
|
||||
Code int `json:"code"`
|
||||
Name string `json:"name"`
|
||||
} `json:"version"`
|
||||
}
|
||||
|
||||
type IndexFileItem struct {
|
||||
Path string
|
||||
Info os.FileInfo
|
||||
@@ -31,7 +47,8 @@ type HTTPStaticServer struct {
|
||||
Title string
|
||||
Theme string
|
||||
PlistProxy string
|
||||
GoogleTrackerId string
|
||||
GoogleTrackerID string
|
||||
AuthType string
|
||||
|
||||
indexes []IndexFileItem
|
||||
m *mux.Router
|
||||
@@ -72,9 +89,12 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
|
||||
// routers for Apple *.ipa
|
||||
m.HandleFunc("/-/ipa/plist/{path:.*}", s.hPlist)
|
||||
m.HandleFunc("/-/ipa/link/{path:.*}", s.hIpaLink)
|
||||
// TODO: /ipa/info
|
||||
|
||||
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET")
|
||||
// TODO: /ipa/info
|
||||
m.HandleFunc("/-/info/{path:.*}", s.hInfo)
|
||||
m.HandleFunc("/-/mkdir/{path:.*}", s.hMkdir)
|
||||
|
||||
m.HandleFunc("/{path:.*}", s.hIndex).Methods("GET", "HEAD")
|
||||
m.HandleFunc("/{path:.*}", s.hUpload).Methods("POST")
|
||||
m.HandleFunc("/{path:.*}", s.hDelete).Methods("DELETE")
|
||||
return s
|
||||
@@ -87,11 +107,20 @@ func (s *HTTPStaticServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
|
||||
finfo, err := os.Stat(relPath)
|
||||
if err == nil && finfo.IsDir() {
|
||||
tmpl.ExecuteTemplate(w, "index", s)
|
||||
log.Println("GET", path, relPath)
|
||||
if r.FormValue("raw") == "false" || isDir(relPath) {
|
||||
if r.Method == "HEAD" {
|
||||
return
|
||||
}
|
||||
renderHTML(w, "index.html", s)
|
||||
} else {
|
||||
if filepath.Base(path) == YAMLCONF {
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.Delete {
|
||||
http.Error(w, "Security warning, not allowed to read", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.FormValue("download") == "true" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(filepath.Base(path)))
|
||||
}
|
||||
@@ -105,12 +134,32 @@ func (s *HTTPStaticServer) hStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hMkdir(w http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Mkdir forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
name := req.FormValue("name")
|
||||
if strings.ContainsAny(name, "\\/:*<>|") {
|
||||
http.Error(w, "Name should not contains \\/:*<>|", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
err := os.Mkdir(filepath.Join(s.Root, path, name), 0755)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Success"))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
|
||||
// only can delete file now
|
||||
path := mux.Vars(req)["path"]
|
||||
auth := s.readAccessConf(path)
|
||||
log.Printf("%#v", auth)
|
||||
if !auth.Delete {
|
||||
if !auth.canDelete(req) {
|
||||
http.Error(w, "Delete forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -124,46 +173,106 @@ func (s *HTTPStaticServer) hDelete(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func (s *HTTPStaticServer) hUpload(w http.ResponseWriter, req *http.Request) {
|
||||
path := mux.Vars(req)["path"]
|
||||
dirpath := filepath.Join(s.Root, path)
|
||||
|
||||
// check auth
|
||||
auth := s.readAccessConf(path)
|
||||
if !auth.Upload {
|
||||
if !auth.canUpload(req) {
|
||||
http.Error(w, "Upload forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err := req.ParseMultipartForm(1 << 30) // max memory 1G
|
||||
file, header, err := req.FormFile("file")
|
||||
if err != nil {
|
||||
log.Println("Parse form file:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(req.MultipartForm.File["file"]) == 0 {
|
||||
http.Error(w, "Need multipart file", http.StatusInternalServerError)
|
||||
defer func() {
|
||||
file.Close()
|
||||
req.MultipartForm.RemoveAll() // Seen from go source code, req.MultipartForm not nil after call FormFile(..)
|
||||
}()
|
||||
|
||||
// FIXME(ssx): should I check header.Filename here?
|
||||
dstPath := filepath.Join(dirpath, header.Filename)
|
||||
dst, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
log.Println("Create file:", err)
|
||||
http.Error(w, "File create "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
dirpath := filepath.Join(s.Root, path)
|
||||
|
||||
for _, mfile := range req.MultipartForm.File["file"] {
|
||||
file, err := mfile.Open()
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dst, err := os.Create(filepath.Join(dirpath, mfile.Filename)) // BUG(ssx): There is a leak here
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
log.Println("Handle upload file:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
log.Println("Handle upload file:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Upload success"))
|
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"destination": dstPath,
|
||||
})
|
||||
}
|
||||
|
||||
type FileJSONInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
Path string `json:"path"`
|
||||
ModTime int64 `json:"mtime"`
|
||||
Extra interface{} `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// path should be absolute
|
||||
func parseApkInfo(path string) (ai *ApkInfo) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("parse-apk-info panic:", err)
|
||||
}
|
||||
}()
|
||||
apkf, err := apk.OpenFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ai = &ApkInfo{}
|
||||
ai.MainActivity, _ = apkf.MainActivity()
|
||||
ai.PackageName = apkf.PackageName()
|
||||
ai.Version.Code = apkf.Manifest().VersionCode
|
||||
ai.Version.Name = apkf.Manifest().VersionName
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hInfo(w http.ResponseWriter, r *http.Request) {
|
||||
path := mux.Vars(r)["path"]
|
||||
relPath := filepath.Join(s.Root, path)
|
||||
if !isFile(relPath) {
|
||||
http.Error(w, "Not a file", 403)
|
||||
return
|
||||
}
|
||||
fi, err := os.Stat(relPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
fji := &FileJSONInfo{
|
||||
Name: fi.Name(),
|
||||
Size: fi.Size(),
|
||||
Path: path,
|
||||
ModTime: fi.ModTime().UnixNano() / 1e6,
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".md":
|
||||
fji.Type = "markdown"
|
||||
case ".apk":
|
||||
fji.Type = "apk"
|
||||
fji.Extra = parseApkInfo(relPath)
|
||||
default:
|
||||
fji.Type = "text"
|
||||
}
|
||||
data, _ := json.Marshal(fji)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -240,17 +349,14 @@ func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
plistUrl = url
|
||||
//plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
|
||||
log.Println("PlistURL:", plistUrl)
|
||||
renderHTML(w, "ipa-install.html", map[string]string{
|
||||
"Name": filepath.Base(path),
|
||||
"PlistLink": plistUrl,
|
||||
})
|
||||
// w.Write([]byte(fmt.Sprintf(
|
||||
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
|
||||
// plistUrl)))
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) genPlistLink(httpPlistLink string) (plistUrl string, err error) {
|
||||
@@ -286,23 +392,111 @@ func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Reque
|
||||
http.ServeFile(w, r, filepath.Join(s.Root, path))
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
type HTTPFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Size string `json:"size"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime int64 `json:"mtime"`
|
||||
}
|
||||
|
||||
type AccessTable struct {
|
||||
Regex string `yaml:"regex"`
|
||||
Allow bool `yaml:"allow"`
|
||||
}
|
||||
|
||||
type UserControl struct {
|
||||
Email string
|
||||
// Access bool
|
||||
Upload bool
|
||||
Delete bool
|
||||
Token string
|
||||
}
|
||||
|
||||
type AccessConf struct {
|
||||
Upload bool `yaml:"upload" json:"upload"`
|
||||
Delete bool `yaml:"delete" json:"delete"`
|
||||
Upload bool `yaml:"upload" json:"upload"`
|
||||
Delete bool `yaml:"delete" json:"delete"`
|
||||
Users []UserControl `yaml:"users" json:"users"`
|
||||
AccessTables []AccessTable `yaml:"accessTables"`
|
||||
}
|
||||
|
||||
var reCache = make(map[string]*regexp.Regexp)
|
||||
|
||||
func (c *AccessConf) canAccess(fileName string) bool {
|
||||
for _, table := range c.AccessTables {
|
||||
pattern, ok := reCache[table.Regex]
|
||||
if !ok {
|
||||
pattern, _ = regexp.Compile(table.Regex)
|
||||
reCache[table.Regex] = pattern
|
||||
}
|
||||
// skip wrong format regex
|
||||
if pattern == nil {
|
||||
continue
|
||||
}
|
||||
if pattern.MatchString(fileName) {
|
||||
return table.Allow
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AccessConf) canDelete(r *http.Request) bool {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
return c.Delete
|
||||
}
|
||||
val := session.Values["user"]
|
||||
if val == nil {
|
||||
return c.Delete
|
||||
}
|
||||
userInfo := val.(*UserInfo)
|
||||
for _, rule := range c.Users {
|
||||
if rule.Email == userInfo.Email {
|
||||
return rule.Delete
|
||||
}
|
||||
}
|
||||
return c.Delete
|
||||
}
|
||||
|
||||
func (c *AccessConf) canUploadByToken(token string) bool {
|
||||
for _, rule := range c.Users {
|
||||
if rule.Token == token {
|
||||
return rule.Upload
|
||||
}
|
||||
}
|
||||
return c.Upload
|
||||
}
|
||||
|
||||
func (c *AccessConf) canUpload(r *http.Request) bool {
|
||||
token := r.FormValue("token")
|
||||
if token != "" {
|
||||
return c.canUploadByToken(token)
|
||||
}
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
return c.Upload
|
||||
}
|
||||
val := session.Values["user"]
|
||||
if val == nil {
|
||||
return c.Upload
|
||||
}
|
||||
userInfo := val.(*UserInfo)
|
||||
|
||||
for _, rule := range c.Users {
|
||||
if rule.Email == userInfo.Email {
|
||||
return rule.Upload
|
||||
}
|
||||
}
|
||||
return c.Upload
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath := mux.Vars(r)["path"]
|
||||
localPath := filepath.Join(s.Root, requestPath)
|
||||
search := r.FormValue("search")
|
||||
auth := s.readAccessConf(requestPath)
|
||||
auth.Upload = auth.canUpload(r)
|
||||
auth.Delete = auth.canDelete(r)
|
||||
|
||||
// path string -> info os.FileInfo
|
||||
fileInfoMap := make(map[string]os.FileInfo, 0)
|
||||
@@ -329,9 +523,12 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// turn file list -> json
|
||||
lrs := make([]ListResponse, 0)
|
||||
lrs := make([]HTTPFileInfo, 0)
|
||||
for path, info := range fileInfoMap {
|
||||
lr := ListResponse{
|
||||
if !auth.canAccess(info.Name()) {
|
||||
continue
|
||||
}
|
||||
lr := HTTPFileInfo{
|
||||
Name: info.Name(),
|
||||
Path: path,
|
||||
ModTime: info.ModTime().UnixNano() / 1e6,
|
||||
@@ -348,39 +545,60 @@ func (s *HTTPStaticServer) hJSONList(w http.ResponseWriter, r *http.Request) {
|
||||
lr.Name = name
|
||||
lr.Path = filepath.Join(filepath.Dir(path), name)
|
||||
lr.Type = "dir"
|
||||
lr.Size = "-"
|
||||
lr.Size = s.historyDirSize(lr.Path)
|
||||
} else {
|
||||
lr.Type = "file"
|
||||
lr.Size = formatSize(info)
|
||||
lr.Size = info.Size() // formatSize(info)
|
||||
}
|
||||
lrs = append(lrs, lr)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"files": lrs,
|
||||
"auth": s.readAccessConf(requestPath),
|
||||
"auth": auth,
|
||||
})
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
var dirSizeMap = make(map[string]int64)
|
||||
|
||||
func (s *HTTPStaticServer) makeIndex() error {
|
||||
var indexes = make([]IndexFileItem, 0)
|
||||
var err = filepath.Walk(s.Root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("WARN: Visit path: %s error: %v", strconv.Quote(path), err)
|
||||
return filepath.SkipDir
|
||||
// return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
path, _ = filepath.Rel(s.Root, path)
|
||||
}
|
||||
|
||||
path, _ = filepath.Rel(s.Root, path)
|
||||
path = filepath.ToSlash(path)
|
||||
indexes = append(indexes, IndexFileItem{path, info})
|
||||
return nil
|
||||
})
|
||||
s.indexes = indexes
|
||||
dirSizeMap = make(map[string]int64)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) historyDirSize(dir string) int64 {
|
||||
var size int64
|
||||
if size, ok := dirSizeMap[dir]; ok {
|
||||
return size
|
||||
}
|
||||
for _, fitem := range s.indexes {
|
||||
if filepath.HasPrefix(fitem.Path, dir) {
|
||||
size += fitem.Info.Size()
|
||||
}
|
||||
}
|
||||
dirSizeMap[dir] = size
|
||||
return size
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
|
||||
ret := make([]IndexFileItem, 0)
|
||||
for _, item := range s.indexes {
|
||||
@@ -410,16 +628,23 @@ func (s *HTTPStaticServer) findIndex(text string) []IndexFileItem {
|
||||
func (s *HTTPStaticServer) defaultAccessConf() AccessConf {
|
||||
return AccessConf{
|
||||
Upload: s.Upload,
|
||||
Delete: s.Delete,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPStaticServer) readAccessConf(requestPath string) (ac AccessConf) {
|
||||
ac = s.defaultAccessConf()
|
||||
requestPath = filepath.Clean(requestPath)
|
||||
if requestPath == "/" || requestPath == "" || requestPath == "." {
|
||||
ac = s.defaultAccessConf()
|
||||
} else {
|
||||
parentPath := filepath.Dir(requestPath)
|
||||
ac = s.readAccessConf(parentPath)
|
||||
}
|
||||
relPath := filepath.Join(s.Root, requestPath)
|
||||
if isFile(relPath) {
|
||||
relPath = filepath.Dir(relPath)
|
||||
}
|
||||
cfgFile := filepath.Join(relPath, ".ghs.yml")
|
||||
cfgFile := filepath.Join(relPath, YAMLCONF)
|
||||
data, err := ioutil.ReadFile(cfgFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -456,3 +681,66 @@ func isFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.Mode().IsDir()
|
||||
}
|
||||
|
||||
func assetsContent(name string) string {
|
||||
fd, err := Assets.Open(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
data, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// TODO: I need to read more abouthtml/template
|
||||
var (
|
||||
funcMap template.FuncMap
|
||||
)
|
||||
|
||||
func init() {
|
||||
funcMap = template.FuncMap{
|
||||
"title": strings.Title,
|
||||
"urlhash": func(path string) string {
|
||||
httpFile, err := Assets.Open(path)
|
||||
if err != nil {
|
||||
return path + "#no-such-file"
|
||||
}
|
||||
info, err := httpFile.Stat()
|
||||
if err != nil {
|
||||
return path + "#stat-error"
|
||||
}
|
||||
return fmt.Sprintf("%s?t=%d", path, info.ModTime().Unix())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_tmpls = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
func executeTemplate(w http.ResponseWriter, name string, v interface{}) {
|
||||
if t, ok := _tmpls[name]; ok {
|
||||
t.Execute(w, v)
|
||||
return
|
||||
}
|
||||
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
|
||||
_tmpls[name] = t
|
||||
t.Execute(w, v)
|
||||
}
|
||||
|
||||
func renderHTML(w http.ResponseWriter, name string, v interface{}) {
|
||||
if _, ok := Assets.(http.Dir); ok {
|
||||
log.Println("Hot load", name)
|
||||
t := template.Must(template.New(name).Funcs(funcMap).Delims("[[", "]]").Parse(assetsContent(name)))
|
||||
t.Execute(w, v)
|
||||
} else {
|
||||
executeTemplate(w, name, v)
|
||||
}
|
||||
}
|
||||
|
||||
71
main.go
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -15,40 +16,50 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
accesslog "github.com/codeskyblue/go-accesslog"
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/goji/httpauth"
|
||||
"github.com/gorilla/handlers"
|
||||
accesslog "github.com/mash/go-accesslog"
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
)
|
||||
|
||||
type Configure struct {
|
||||
Conf *os.File `yaml:"-"`
|
||||
Addr string `yaml:"addr"`
|
||||
Port int `yaml:"port"`
|
||||
Root string `yaml:"root"`
|
||||
HttpAuth string `yaml:"httpauth"`
|
||||
HTTPAuth string `yaml:"httpauth"`
|
||||
Cert string `yaml:"cert"`
|
||||
Key string `yaml:"key"`
|
||||
Cors bool `yaml:"cors"`
|
||||
Theme string `yaml:"theme"`
|
||||
XHeaders bool `yaml:"xheaders"`
|
||||
Upload bool `yaml:"upload"`
|
||||
Delete bool `yaml:"delete"`
|
||||
PlistProxy string `yaml:"plistproxy"`
|
||||
Title string `yaml:"title"`
|
||||
Debug bool `yaml:"debug"`
|
||||
GoogleTrackerId string `yaml:"google-tracker-id"`
|
||||
GoogleTrackerID string `yaml:"google-tracker-id"`
|
||||
Auth struct {
|
||||
Type string `yaml:"type"` // openid|http|github
|
||||
OpenID string `yaml:"openid"`
|
||||
HTTP string `yaml:"http"`
|
||||
ID string `yaml:"id"` // for oauth2
|
||||
Secret string `yaml:"secret"` // for oauth2
|
||||
} `yaml:"auth"`
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
}
|
||||
type httpLogger struct{}
|
||||
|
||||
func (l logger) Log(record accesslog.LogRecord) {
|
||||
func (l httpLogger) Log(record accesslog.LogRecord) {
|
||||
log.Printf("%s - %s %d %s", record.Ip, record.Method, record.Status, record.Uri)
|
||||
}
|
||||
|
||||
var (
|
||||
defaultPlistProxy = "https://plistproxy.herokuapp.com/plist"
|
||||
defaultOpenID = "https://login.netease.com/openid"
|
||||
gcfg = Configure{}
|
||||
l = logger{}
|
||||
logger = httpLogger{}
|
||||
|
||||
VERSION = "unknown"
|
||||
BUILDTIME = "unknown time"
|
||||
@@ -79,28 +90,34 @@ func versionMessage() string {
|
||||
func parseFlags() error {
|
||||
// initial default conf
|
||||
gcfg.Root = "./"
|
||||
gcfg.Addr = ":8000"
|
||||
gcfg.Port = 8000
|
||||
gcfg.Addr = ""
|
||||
gcfg.Theme = "black"
|
||||
gcfg.PlistProxy = defaultPlistProxy
|
||||
gcfg.GoogleTrackerId = "UA-81205425-2"
|
||||
gcfg.Auth.OpenID = defaultOpenID
|
||||
gcfg.GoogleTrackerID = "UA-81205425-2"
|
||||
gcfg.Title = "Go HTTP File Server"
|
||||
|
||||
kingpin.HelpFlag.Short('h')
|
||||
kingpin.Version(versionMessage())
|
||||
kingpin.Flag("conf", "config file path, yaml format").FileVar(&gcfg.Conf)
|
||||
kingpin.Flag("root", "root directory, default ./").Short('r').StringVar(&gcfg.Root)
|
||||
kingpin.Flag("addr", "listen address, default :8000").Short('a').StringVar(&gcfg.Addr)
|
||||
kingpin.Flag("port", "listen port, default 8000").IntVar(&gcfg.Port)
|
||||
kingpin.Flag("addr", "listen address, eg 127.0.0.1:8000").Short('a').StringVar(&gcfg.Addr)
|
||||
kingpin.Flag("cert", "tls cert.pem path").StringVar(&gcfg.Cert)
|
||||
kingpin.Flag("key", "tls key.pem path").StringVar(&gcfg.Key)
|
||||
kingpin.Flag("httpauth", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.HttpAuth)
|
||||
kingpin.Flag("auth-type", "Auth type <http|openid>").StringVar(&gcfg.Auth.Type)
|
||||
kingpin.Flag("auth-http", "HTTP basic auth (ex: user:pass)").StringVar(&gcfg.Auth.HTTP)
|
||||
kingpin.Flag("auth-openid", "OpenID auth identity url").StringVar(&gcfg.Auth.OpenID)
|
||||
kingpin.Flag("theme", "web theme, one of <black|green>").StringVar(&gcfg.Theme)
|
||||
kingpin.Flag("upload", "enable upload support").BoolVar(&gcfg.Upload)
|
||||
kingpin.Flag("delete", "enable delete support").BoolVar(&gcfg.Delete)
|
||||
kingpin.Flag("xheaders", "used when behide nginx").BoolVar(&gcfg.XHeaders)
|
||||
kingpin.Flag("cors", "enable cross-site HTTP request").BoolVar(&gcfg.Cors)
|
||||
kingpin.Flag("debug", "enable debug mode").BoolVar(&gcfg.Debug)
|
||||
kingpin.Flag("plistproxy", "plist proxy when server is not https").Short('p').StringVar(&gcfg.PlistProxy)
|
||||
kingpin.Flag("title", "server title").StringVar(&gcfg.Title)
|
||||
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerId)
|
||||
kingpin.Flag("google-tracker-id", "set to empty to disable it").StringVar(&gcfg.GoogleTrackerID)
|
||||
|
||||
kingpin.Parse() // first parse conf
|
||||
|
||||
@@ -125,12 +142,15 @@ func main() {
|
||||
data, _ := yaml.Marshal(gcfg)
|
||||
fmt.Printf("--- config ---\n%s\n", string(data))
|
||||
}
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
||||
|
||||
ss := NewHTTPStaticServer(gcfg.Root)
|
||||
ss.Theme = gcfg.Theme
|
||||
ss.Title = gcfg.Title
|
||||
ss.GoogleTrackerId = gcfg.GoogleTrackerId
|
||||
ss.GoogleTrackerID = gcfg.GoogleTrackerID
|
||||
ss.Upload = gcfg.Upload
|
||||
ss.Delete = gcfg.Delete
|
||||
ss.AuthType = gcfg.Auth.Type
|
||||
|
||||
if gcfg.PlistProxy != "" {
|
||||
u, err := url.Parse(gcfg.PlistProxy)
|
||||
@@ -143,14 +163,22 @@ func main() {
|
||||
|
||||
var hdlr http.Handler = ss
|
||||
|
||||
hdlr = accesslog.NewLoggingHandler(hdlr, l)
|
||||
hdlr = accesslog.NewLoggingHandler(hdlr, logger)
|
||||
|
||||
// HTTP Basic Authentication
|
||||
userpass := strings.SplitN(gcfg.HttpAuth, ":", 2)
|
||||
if len(userpass) == 2 {
|
||||
user, pass := userpass[0], userpass[1]
|
||||
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
|
||||
userpass := strings.SplitN(gcfg.Auth.HTTP, ":", 2)
|
||||
switch gcfg.Auth.Type {
|
||||
case "http":
|
||||
if len(userpass) == 2 {
|
||||
user, pass := userpass[0], userpass[1]
|
||||
hdlr = httpauth.SimpleBasicAuth(user, pass)(hdlr)
|
||||
}
|
||||
case "openid":
|
||||
handleOpenID(gcfg.Auth.OpenID, false) // FIXME(ssx): set secure default to false
|
||||
// case "github":
|
||||
// handleOAuth2ID(gcfg.Auth.Type, gcfg.Auth.ID, gcfg.Auth.Secret) // FIXME(ssx): set secure default to false
|
||||
}
|
||||
|
||||
// CORS
|
||||
if gcfg.Cors {
|
||||
hdlr = handlers.CORS()(hdlr)
|
||||
@@ -160,6 +188,7 @@ func main() {
|
||||
}
|
||||
|
||||
http.Handle("/", hdlr)
|
||||
http.Handle("/-/assets/", http.StripPrefix("/-/assets/", http.FileServer(Assets)))
|
||||
http.HandleFunc("/-/sysinfo", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
@@ -168,10 +197,14 @@ func main() {
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
if gcfg.Addr == "" {
|
||||
gcfg.Addr = fmt.Sprintf(":%d", gcfg.Port)
|
||||
}
|
||||
if !strings.Contains(gcfg.Addr, ":") {
|
||||
gcfg.Addr = ":" + gcfg.Addr
|
||||
}
|
||||
log.Printf("listening on %s\n", strconv.Quote(gcfg.Addr))
|
||||
_, port, _ := net.SplitHostPort(gcfg.Addr)
|
||||
log.Printf("listening on %s, local address http://%s:%s\n", strconv.Quote(gcfg.Addr), getLocalIP(), port)
|
||||
|
||||
var err error
|
||||
if gcfg.Key != "" && gcfg.Cert != "" {
|
||||
|
||||
112
openid-login.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
openid "github.com/codeskyblue/openid-go"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
var (
|
||||
nonceStore = openid.NewSimpleNonceStore()
|
||||
discoveryCache = openid.NewSimpleDiscoveryCache()
|
||||
store = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||
defaultSessionName = "ghs-session"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
NickName string `json:"nickName"`
|
||||
}
|
||||
|
||||
type M map[string]interface{}
|
||||
|
||||
func init() {
|
||||
gob.Register(&UserInfo{})
|
||||
gob.Register(&M{})
|
||||
}
|
||||
|
||||
func handleOpenID(loginUrl string, secure bool) {
|
||||
http.HandleFunc("/-/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
nextUrl := r.FormValue("next")
|
||||
referer := r.Referer()
|
||||
if nextUrl == "" && strings.Contains(referer, "://"+r.Host) {
|
||||
nextUrl = referer
|
||||
}
|
||||
scheme := "http"
|
||||
if r.URL.Scheme != "" {
|
||||
scheme = r.URL.Scheme
|
||||
}
|
||||
log.Println("Scheme:", scheme)
|
||||
if url, err := openid.RedirectURL(loginUrl,
|
||||
scheme+"://"+r.Host+"/-/openidcallback?next="+nextUrl, ""); err == nil {
|
||||
http.Redirect(w, r, url, 303)
|
||||
} else {
|
||||
log.Println("Should not got error here:", err)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/openidcallback", func(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := openid.Verify("http://"+r.Host+r.URL.String(), discoveryCache, nonceStore)
|
||||
if err != nil {
|
||||
io.WriteString(w, "Authentication check failed.")
|
||||
return
|
||||
}
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user := &UserInfo{
|
||||
Id: id,
|
||||
Email: r.FormValue("openid.sreg.email"),
|
||||
Name: r.FormValue("openid.sreg.fullname"),
|
||||
NickName: r.FormValue("openid.sreg.nickname"),
|
||||
}
|
||||
session.Values["user"] = user
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Println("session save error:", err)
|
||||
}
|
||||
|
||||
nextUrl := r.FormValue("next")
|
||||
if nextUrl == "" {
|
||||
nextUrl = "/"
|
||||
}
|
||||
http.Redirect(w, r, nextUrl, 302)
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
val := session.Values["user"]
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
data, _ := json.Marshal(val)
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
http.HandleFunc("/-/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, defaultSessionName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
delete(session.Values, "user")
|
||||
session.Options.MaxAge = -1
|
||||
nextUrl := r.FormValue("next")
|
||||
_ = session.Save(r, w)
|
||||
if nextUrl == "" {
|
||||
nextUrl = r.Referer()
|
||||
}
|
||||
http.Redirect(w, r, nextUrl, 302)
|
||||
})
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>gohttp server</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/-/res/favicon.png" />
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/bootstrap-3.3.5/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/font-awesome-4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/github-markdown.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/dropzone.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/scrollUp-image.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="/-/res/themes/[[.Theme]].css">
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">[[.Title]]</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="hidden-xs">
|
||||
<a href="javascript:void(0)" v-on:click='genQrcode("/", location.origin)'>
|
||||
View in Phone
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right">
|
||||
<div class="input-group">
|
||||
<input type="text" name="search" class="form-control" placeholder="Search text" v-bind:value="search" autofocus>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button">
|
||||
<span class="glyphicon glyphicon-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a v-on:click='changePath("/", $event)' href="/"><i class="fa fa-home"></i></a>
|
||||
</li>
|
||||
<li v-for="bc in breadcrumb.slice(0, breadcrumb.length-1)">
|
||||
<a v-on:click='changePath(bc.path, $event)' href="{{bc.path}}">{{bc.name}}</a>
|
||||
</li>
|
||||
<li v-if="breadcrumb.length >= 1">
|
||||
{{breadcrumb.slice(-1)[0].name}}
|
||||
</li>
|
||||
</ol>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan=4>
|
||||
<!-- <button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Back <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button> -->
|
||||
<button class="btn btn-xs btn-default" v-on:click='toggleHidden()'>
|
||||
Hidden <i class="fa" v-bind:class='showHidden ? "fa-eye" : "fa-eye-slash"'></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-default" v-if="auth.upload" data-toggle="modal" data-target="#upload-modal">
|
||||
Upload <i class="fa fa-upload"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th class="hidden-xs">
|
||||
<span style="cursor: pointer" v-on:click='mtimeTypeFromNow = !mtimeTypeFromNow'>ModTime</span>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in computedFiles">
|
||||
<td>
|
||||
<a v-on:click='clickFileOrDir(f, $event)' href="/{{f.path}}">
|
||||
<i style="padding-right: 0.5em" class="fa" v-bind:class='genFileClass(f)'></i> {{f.name}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{f.size}}</td>
|
||||
<td class="hidden-xs">{{formatTime(f.mtime)}}</td>
|
||||
<td style="text-align: left">
|
||||
<template v-if="f.type == 'dir'">
|
||||
<a class="btn btn-default btn-xs" href="/-/zip/{{f.path}}">
|
||||
<span class="hidden-xs">Archive</span> Zip
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-if="f.type == 'file'">
|
||||
<a class="btn btn-default btn-xs hidden-xs" href="/{{f.path}}?download=true">
|
||||
<span class="hidden-xs">Download</span>
|
||||
<span class="glyphicon glyphicon-download-alt"></span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-xs hidden-xs" v-on:click="genQrcode(f.name)" href="javascript:void(0)">
|
||||
<span v-if="shouldHaveQrcode(f.name)">QRCode</span>
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
</a>
|
||||
<a class="btn btn-default btn-xs visible-xs" v-if="shouldHaveQrcode(f.name)" href="{{genInstallURL(f.name)}}">
|
||||
Install <i class="fa fa-cube"></i>
|
||||
</a>
|
||||
<a class="btn btn-default btn-xs" v-if="auth.delete" v-on:click="deletePathConfirm(f, $event)" href="javascript:void(0)">
|
||||
<span style="color:#CC3300" class="glyphicon glyphicon-trash"></span>
|
||||
</a>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-12" id="preview" v-if="previewFile">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title" style="font-weight: normal">
|
||||
<i class="fa" v-bind:class='genFileClass(previewFile)'></i>
|
||||
{{previewFile.name}}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<article class="markdown-body">{{{previewFile.contentHTML }}}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" id="content">
|
||||
<!-- Small qrcode modal -->
|
||||
<div id="qrcode-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<span id="qrcode-title"></span>
|
||||
<a style="font-size: 0.6em" href="#" id="qrcode-link">[view]</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="qrcodeCanvas"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload modal-->
|
||||
<div id="upload-modal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-upload"></i> File upload
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="#" class="dropzone" id="my-dropzone"></form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="footer" class="pull-right" style="margin: 2em 1em">
|
||||
<a href="https://github.com/codeskyblue/gohttpserver">gohttpserver (ver:{{version}})</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2016. go1.6
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/-/res/js/jquery-3.1.0.min.js"></script>
|
||||
<script src="/-/res/js/jquery.qrcode.js"></script>
|
||||
<script src="/-/res/js/jquery.scrollUp.min.js"></script>
|
||||
<script src="/-/res/js/qrcode.js"></script>
|
||||
<script src="/-/res/js/vue-1.0.min.js"></script>
|
||||
<script src="/-/res/js/showdown-1.4.2.min.js"></script>
|
||||
<script src="/-/res/js/moment.min.js"></script>
|
||||
<script src="/-/res/js/dropzone.js"></script>
|
||||
<script src="/-/res/js/underscore-min.js"></script>
|
||||
<script src="/-/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="/-/res/js/index.js"></script>
|
||||
[[if .GoogleTrackerId ]]
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '[[.GoogleTrackerId]]', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
[[ end ]]
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>[[.Name]] install</title>
|
||||
<meta http-equiv="Content-Type" content="text/HTML; charset=utf-8">
|
||||
<meta content="target-densitydpi=device-dpi,width=640" name="viewport" id="viewport">
|
||||
<link rel="shortcut icon" type="image/png" href="/-/res/favicon.png" />
|
||||
<script type="text/javascript" src="/-/res/js/ua-parser.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function showById(name) {
|
||||
document.getElementById(name).style.display = 'block';
|
||||
}
|
||||
|
||||
function checkBrowerAndDownload() {
|
||||
var parser = new UAParser();
|
||||
var os_info = parser.getOS();
|
||||
console.log(os_info)
|
||||
|
||||
if (navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == "micromessenger") {
|
||||
showById('wechat');
|
||||
return;
|
||||
}
|
||||
var plistLink = "[[.PlistLink]]";
|
||||
var ipaInstallLink = 'itms-services://?action=download-manifest&url=' + plistLink;
|
||||
|
||||
document.getElementById('itms-link').href = ipaInstallLink;
|
||||
|
||||
if (os_info.name == 'Android') {
|
||||
return;
|
||||
} else if (os_info.name == 'iOS') {
|
||||
showById('safari');
|
||||
|
||||
location.href = ipaInstallLink;
|
||||
return;
|
||||
} else {
|
||||
showById('browser');
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<style>
|
||||
#wechat {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
min-height: 777px;
|
||||
}
|
||||
</style>
|
||||
<div id="wechat" style="display: none">
|
||||
<img style='width: 100%;position: relative;' src='/-/res/imgs/wx.png' />
|
||||
</div>
|
||||
<div id="browser" style="display: none">
|
||||
This is IPA install page, you should open this link with your iPhone.
|
||||
</div>
|
||||
<div id="safari" style="display: none">
|
||||
If install not started soon, click <a id="itms-link" href="#">here</a>
|
||||
</div>
|
||||
<div id="android" style="display: none">
|
||||
This is IPA install page, not for android.
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
checkBrowerAndDownload();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
271
res/js/index.js
@@ -1,271 +0,0 @@
|
||||
jQuery('#qrcodeCanvas').qrcode({
|
||||
text: "http://jetienne.com/"
|
||||
});
|
||||
|
||||
function getExtention(fname) {
|
||||
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
|
||||
}
|
||||
|
||||
function pathJoin(parts, sep) {
|
||||
var separator = sep || '/';
|
||||
var replace = new RegExp(separator + '{1,}', 'g');
|
||||
return parts.join(separator).replace(replace, separator);
|
||||
}
|
||||
|
||||
function getQueryString(name) {
|
||||
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
var r = decodeURI(window.location.search).substr(1).match(reg);
|
||||
if (r != null) return r[2].replace(/\+/g, ' ');
|
||||
return null;
|
||||
}
|
||||
|
||||
var vm = new Vue({
|
||||
el: "#app",
|
||||
data: {
|
||||
message: "Hello vue.js",
|
||||
location: window.location,
|
||||
breadcrumb: [],
|
||||
showHidden: false,
|
||||
previewFile: null,
|
||||
version: "loading",
|
||||
mtimeTypeFromNow: false, // or fromNow
|
||||
auth: {},
|
||||
search: getQueryString("search"),
|
||||
files: [{
|
||||
name: "loading ...",
|
||||
path: "",
|
||||
size: "...",
|
||||
type: "dir",
|
||||
}]
|
||||
},
|
||||
computed: {
|
||||
computedFiles: function() {
|
||||
var that = this;
|
||||
this.previewFile = null;
|
||||
|
||||
var files = this.files.filter(function(f) {
|
||||
if (f.name == 'README.md') {
|
||||
that.previewFile = {
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
size: f.size,
|
||||
type: 'markdown',
|
||||
contentHTML: '',
|
||||
}
|
||||
}
|
||||
if (!that.showHidden && f.name.slice(0, 1) === '.') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// console.log(this.previewFile)
|
||||
if (this.previewFile) {
|
||||
var name = this.previewFile.name; // For now only README.md
|
||||
console.log(pathJoin([location.pathname, 'README.md']))
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, 'README.md']),
|
||||
method: 'GET',
|
||||
success: function(res) {
|
||||
var converter = new showdown.Converter({
|
||||
tables: true,
|
||||
omitExtraWLInCodeBlocks: true,
|
||||
parseImgDimensions: true,
|
||||
simplifiedAutoLink: true,
|
||||
literalMidWordUnderscores: true,
|
||||
tasklists: true,
|
||||
ghCodeBlocks: true,
|
||||
smoothLivePreview: true,
|
||||
});
|
||||
|
||||
var html = converter.makeHtml(res);
|
||||
that.previewFile.contentHTML = html;
|
||||
},
|
||||
error: function(err) {
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return files;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatTime: function(timestamp) {
|
||||
var m = moment(timestamp);
|
||||
if (this.mtimeTypeFromNow) {
|
||||
return m.fromNow();
|
||||
}
|
||||
return m.format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
toggleHidden: function() {
|
||||
this.showHidden = !this.showHidden;
|
||||
},
|
||||
genInstallURL: function(name) {
|
||||
if (getExtention(name) == "ipa") {
|
||||
urlPath = location.protocol + "//" + pathJoin([location.host, "/-/ipa/link", location.pathname, name]);
|
||||
return urlPath;
|
||||
}
|
||||
return location.protocol + "//" + pathJoin([location.host, location.pathname, name]);
|
||||
},
|
||||
genQrcode: function(text, title) {
|
||||
var urlPath = this.genInstallURL(text);
|
||||
$("#qrcode-title").html(title || text);
|
||||
$("#qrcode-link").attr("href", urlPath);
|
||||
$('#qrcodeCanvas').empty().qrcode({
|
||||
text: urlPath
|
||||
});
|
||||
$("#qrcode-modal").modal("show");
|
||||
},
|
||||
shouldHaveQrcode: function(name) {
|
||||
return ['apk', 'ipa'].indexOf(getExtention(name)) !== -1;
|
||||
},
|
||||
genFileClass: function(f) {
|
||||
if (f.type == "dir") {
|
||||
if (f.name == '.git') {
|
||||
return 'fa-git-square';
|
||||
}
|
||||
return "fa-folder-open";
|
||||
}
|
||||
var ext = getExtention(f.name);
|
||||
switch (ext) {
|
||||
case "go":
|
||||
case "py":
|
||||
case "js":
|
||||
case "java":
|
||||
case "c":
|
||||
case "cpp":
|
||||
case "h":
|
||||
return "fa-file-code-o";
|
||||
case "pdf":
|
||||
return "fa-file-pdf-o";
|
||||
case "zip":
|
||||
return "fa-file-zip-o";
|
||||
case "mp3":
|
||||
case "wav":
|
||||
return "fa-file-audio-o";
|
||||
case "jpg":
|
||||
case "png":
|
||||
case "gif":
|
||||
case "jpeg":
|
||||
case "tiff":
|
||||
return "fa-file-picture-o";
|
||||
case "ipa":
|
||||
case "dmg":
|
||||
return "fa-apple";
|
||||
case "apk":
|
||||
return "fa-android";
|
||||
case "exe":
|
||||
return "fa-windows";
|
||||
}
|
||||
return "fa-file-text-o"
|
||||
},
|
||||
clickFileOrDir: function(f, e) {
|
||||
if (f.type == "file") {
|
||||
return true;
|
||||
}
|
||||
var reqPath = pathJoin([location.pathname, f.name]);
|
||||
loadDirectory(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
changePath: function(reqPath, e) {
|
||||
loadDirectory(reqPath);
|
||||
e.preventDefault()
|
||||
},
|
||||
deletePathConfirm: function(f, e) {
|
||||
// confirm
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: pathJoin([location.pathname, f.name]),
|
||||
method: 'DELETE',
|
||||
success: function(res) {
|
||||
loadFileList()
|
||||
},
|
||||
error: function(err) {
|
||||
alert(err.responseText);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateBreadcrumb: function() {
|
||||
var pathname = decodeURI(location.pathname || "/");
|
||||
var parts = pathname.split('/');
|
||||
this.breadcrumb = [];
|
||||
if (pathname == "/") {
|
||||
return this.breadcrumb;
|
||||
}
|
||||
var i = 2;
|
||||
for (; i <= parts.length; i += 1) {
|
||||
var name = parts[i - 1];
|
||||
var path = parts.slice(0, i).join('/');
|
||||
this.breadcrumb.push({
|
||||
name: name + (i == parts.length ? ' /' : ''),
|
||||
path: path
|
||||
})
|
||||
}
|
||||
return this.breadcrumb;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.onpopstate = function(event) {
|
||||
var pathname = decodeURI(location.pathname)
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
function loadDirectory(reqPath) {
|
||||
window.history.pushState({}, "", reqPath);
|
||||
loadFileList(reqPath)
|
||||
}
|
||||
|
||||
function loadFileList(pathname) {
|
||||
var pathname = pathname || location.pathname;
|
||||
// console.log("load filelist:", pathname)
|
||||
$.ajax({
|
||||
url: pathJoin(["/-/json", pathname]),
|
||||
dataType: "json",
|
||||
cache: false,
|
||||
success: function(res) {
|
||||
res.files = _.sortBy(res.files, function(f) {
|
||||
return [f.type, f.name];
|
||||
})
|
||||
|
||||
vm.files = res.files;
|
||||
vm.auth = res.auth;
|
||||
},
|
||||
error: function(err) {
|
||||
console.error(err)
|
||||
},
|
||||
});
|
||||
vm.updateBreadcrumb();
|
||||
}
|
||||
|
||||
// For page first loading
|
||||
loadFileList(location.pathname + location.search)
|
||||
|
||||
// update version
|
||||
$.getJSON("/-/sysinfo", function(res) {
|
||||
vm.version = res.version;
|
||||
})
|
||||
|
||||
Dropzone.options.myDropzone = {
|
||||
paramName: "file",
|
||||
maxFilesize: 1024,
|
||||
addRemoveLinks: true,
|
||||
init: function() {
|
||||
this.on("uploadprogress", function(file, progress) {
|
||||
console.log("File progress", progress);
|
||||
});
|
||||
this.on("complete", function(file) {
|
||||
console.log("reload file list")
|
||||
loadFileList()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$.scrollUp({
|
||||
scrollText: '', // text are defined in css
|
||||
});
|
||||
});
|
||||
Vue.filter('fromNow', function(value) {
|
||||
return moment(value).fromNow();
|
||||
})
|
||||
4
res/js/showdown-1.4.2.min.js
vendored
@@ -1,20 +0,0 @@
|
||||
// +build bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS())))
|
||||
|
||||
for name, path := range templates {
|
||||
data, err := Asset(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ParseTemplate(name, string(data))
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// +build !bindata
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
//selfDir := filepath.Dir(os.Args[0])
|
||||
//resDir := filepath.Join(selfDir, "./res")
|
||||
resDir := "./res"
|
||||
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(http.Dir(resDir))))
|
||||
|
||||
for name, path := range templates {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ParseTemplate(name, string(content))
|
||||
}
|
||||
}
|
||||
5
testdata/deletable/.ghs.yml
vendored
@@ -1,2 +1,7 @@
|
||||
upload: true
|
||||
delete: true
|
||||
accessTables:
|
||||
- regex: block.file
|
||||
allow: false
|
||||
- regex: visual.file
|
||||
allow: true
|
||||
0
testdata/deletable/block.file
vendored
Normal file
0
testdata/deletable/other.file
vendored
Normal file
0
testdata/deletable/visual.file
vendored
Normal file
7
testdata/uploadable/.ghs.yml
vendored
@@ -1,2 +1,7 @@
|
||||
---
|
||||
upload: true
|
||||
upload: false
|
||||
users:
|
||||
- email: "user@example.com"
|
||||
upload: true
|
||||
delete: true
|
||||
token: 123456
|
||||
0
testdata/uploadable/sub-upload/.gitkeep
vendored
Normal file
51
utils.go
@@ -1,28 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func formatSize(file os.FileInfo) string {
|
||||
if file.IsDir() {
|
||||
return "-"
|
||||
}
|
||||
size := file.Size()
|
||||
switch {
|
||||
case size > 1024*1024:
|
||||
return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
|
||||
case size > 1024:
|
||||
return fmt.Sprintf("%.1f KB", float64(size)/1024)
|
||||
default:
|
||||
return strconv.Itoa(int(size)) + " B"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// func formatSize(file os.FileInfo) string {
|
||||
// if file.IsDir() {
|
||||
// return "-"
|
||||
// }
|
||||
// size := file.Size()
|
||||
// switch {
|
||||
// case size > 1024*1024:
|
||||
// return fmt.Sprintf("%.1f MB", float64(size)/1024/1024)
|
||||
// case size > 1024:
|
||||
// return fmt.Sprintf("%.1f KB", float64(size)/1024)
|
||||
// default:
|
||||
// return strconv.Itoa(int(size)) + " B"
|
||||
// }
|
||||
// return ""
|
||||
// }
|
||||
|
||||
func getRealIP(req *http.Request) string {
|
||||
xip := req.Header.Get("X-Real-IP")
|
||||
@@ -56,3 +54,20 @@ func SublimeContains(s, substr string) bool {
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// getLocalIP returns the non loopback local IP of the host
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
6
vendor/github.com/DHowett/go-plist/README.md
generated
vendored
@@ -1,6 +1,8 @@
|
||||
# plist - A pure Go property list transcoder
|
||||
# plist - A pure Go property list transcoder [](https://gitlab.howett.net/go/plist/commits/master)
|
||||
## INSTALL
|
||||
$ go get howett.net/plist
|
||||
```
|
||||
$ go get howett.net/plist
|
||||
```
|
||||
|
||||
## FEATURES
|
||||
* Supports encoding/decoding property lists (Apple XML, Apple Binary, OpenStep and GNUStep) from/to arbitrary Go types
|
||||
|
||||
520
vendor/github.com/DHowett/go-plist/bplist.go
generated
vendored
@@ -1,18 +1,5 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"runtime"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
type bplistTrailer struct {
|
||||
Unused [5]uint8
|
||||
SortVersion uint8
|
||||
@@ -37,510 +24,3 @@ const (
|
||||
bpTagArray = 0xA0
|
||||
bpTagDictionary = 0xD0
|
||||
)
|
||||
|
||||
type bplistGenerator struct {
|
||||
writer *countedWriter
|
||||
uniqmap map[interface{}]uint64
|
||||
objmap map[*plistValue]uint64
|
||||
objtable []*plistValue
|
||||
nobjects uint64
|
||||
trailer bplistTrailer
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) flattenPlistValue(pval *plistValue) {
|
||||
switch pval.kind {
|
||||
case String, Integer, Real:
|
||||
if _, ok := p.uniqmap[pval.value]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[pval.value] = p.nobjects
|
||||
case Date:
|
||||
k := pval.value.(time.Time).UnixNano()
|
||||
if _, ok := p.uniqmap[k]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[k] = p.nobjects
|
||||
case Data:
|
||||
// Data are uniqued by their checksums.
|
||||
// The wonderful difference between uint64 (which we use for numbers)
|
||||
// and uint32 makes this possible.
|
||||
// Todo: Look at calculating this only once and storing it somewhere;
|
||||
// crc32 is fairly quick, however.
|
||||
uniqkey := crc32.ChecksumIEEE(pval.value.([]byte))
|
||||
if _, ok := p.uniqmap[uniqkey]; ok {
|
||||
return
|
||||
}
|
||||
p.uniqmap[uniqkey] = p.nobjects
|
||||
}
|
||||
|
||||
p.objtable = append(p.objtable, pval)
|
||||
p.objmap[pval] = p.nobjects
|
||||
p.nobjects++
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
dict := pval.value.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for _, k := range dict.keys {
|
||||
p.flattenPlistValue(&plistValue{String, k})
|
||||
}
|
||||
for _, v := range dict.values {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
case Array:
|
||||
subvalues := pval.value.([]*plistValue)
|
||||
for _, v := range subvalues {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) indexForPlistValue(pval *plistValue) (uint64, bool) {
|
||||
var v uint64
|
||||
var ok bool
|
||||
switch pval.kind {
|
||||
case String, Integer, Real:
|
||||
v, ok = p.uniqmap[pval.value]
|
||||
case Date:
|
||||
v, ok = p.uniqmap[pval.value.(time.Time).UnixNano()]
|
||||
case Data:
|
||||
v, ok = p.uniqmap[crc32.ChecksumIEEE(pval.value.([]byte))]
|
||||
default:
|
||||
v, ok = p.objmap[pval]
|
||||
}
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) generateDocument(rootpval *plistValue) {
|
||||
p.objtable = make([]*plistValue, 0, 15)
|
||||
p.uniqmap = make(map[interface{}]uint64)
|
||||
p.objmap = make(map[*plistValue]uint64)
|
||||
p.flattenPlistValue(rootpval)
|
||||
|
||||
p.trailer.NumObjects = uint64(len(p.objtable))
|
||||
p.trailer.ObjectRefSize = uint8(minimumSizeForInt(p.trailer.NumObjects))
|
||||
|
||||
p.writer.Write([]byte("bplist00"))
|
||||
|
||||
offtable := make([]uint64, p.trailer.NumObjects)
|
||||
for i, pval := range p.objtable {
|
||||
offtable[i] = uint64(p.writer.BytesWritten())
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
p.trailer.OffsetIntSize = uint8(minimumSizeForInt(uint64(p.writer.BytesWritten())))
|
||||
p.trailer.TopObject = p.objmap[rootpval]
|
||||
p.trailer.OffsetTableOffset = uint64(p.writer.BytesWritten())
|
||||
|
||||
for _, offset := range offtable {
|
||||
p.writeSizedInt(offset, int(p.trailer.OffsetIntSize))
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, p.trailer)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
p.writeDictionaryTag(pval.value.(*dictionary))
|
||||
case Array:
|
||||
p.writeArrayTag(pval.value.([]*plistValue))
|
||||
case String:
|
||||
p.writeStringTag(pval.value.(string))
|
||||
case Integer:
|
||||
p.writeIntTag(pval.value.(signedInt).value)
|
||||
case Real:
|
||||
p.writeRealTag(pval.value.(sizedFloat).value, pval.value.(sizedFloat).bits)
|
||||
case Boolean:
|
||||
p.writeBoolTag(pval.value.(bool))
|
||||
case Data:
|
||||
p.writeDataTag(pval.value.([]byte))
|
||||
case Date:
|
||||
p.writeDateTag(pval.value.(time.Time))
|
||||
}
|
||||
}
|
||||
|
||||
func minimumSizeForInt(n uint64) int {
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
return 1
|
||||
case n <= uint64(0xffff):
|
||||
return 2
|
||||
case n <= uint64(0xffffffff):
|
||||
return 4
|
||||
default:
|
||||
return 8
|
||||
}
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeSizedInt(n uint64, nbytes int) {
|
||||
var val interface{}
|
||||
switch nbytes {
|
||||
case 1:
|
||||
val = uint8(n)
|
||||
case 2:
|
||||
val = uint16(n)
|
||||
case 4:
|
||||
val = uint32(n)
|
||||
case 8:
|
||||
val = n
|
||||
default:
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeBoolTag(v bool) {
|
||||
tag := uint8(bpTagBoolFalse)
|
||||
if v {
|
||||
tag = bpTagBoolTrue
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeIntTag(n uint64) {
|
||||
var tag uint8
|
||||
var val interface{}
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
val = uint8(n)
|
||||
tag = bpTagInteger | 0x0
|
||||
case n <= uint64(0xffff):
|
||||
val = uint16(n)
|
||||
tag = bpTagInteger | 0x1
|
||||
case n <= uint64(0xffffffff):
|
||||
val = uint32(n)
|
||||
tag = bpTagInteger | 0x2
|
||||
default:
|
||||
val = n
|
||||
tag = bpTagInteger | 0x3
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeRealTag(n float64, bits int) {
|
||||
var tag uint8 = bpTagReal | 0x3
|
||||
var val interface{} = n
|
||||
if bits == 32 {
|
||||
val = float32(n)
|
||||
tag = bpTagReal | 0x2
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDateTag(t time.Time) {
|
||||
tag := uint8(bpTagDate) | 0x3
|
||||
val := float64(t.In(time.UTC).UnixNano()) / float64(time.Second)
|
||||
val -= 978307200 // Adjust to Apple Epoch
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeCountedTag(tag uint8, count uint64) {
|
||||
marker := tag
|
||||
if count >= 0xF {
|
||||
marker |= 0xF
|
||||
} else {
|
||||
marker |= uint8(count)
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, marker)
|
||||
|
||||
if count >= 0xF {
|
||||
p.writeIntTag(count)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDataTag(data []byte) {
|
||||
p.writeCountedTag(bpTagData, uint64(len(data)))
|
||||
binary.Write(p.writer, binary.BigEndian, data)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeStringTag(str string) {
|
||||
for _, r := range str {
|
||||
if r > 0xFF {
|
||||
utf16Runes := utf16.Encode([]rune(str))
|
||||
p.writeCountedTag(bpTagUTF16String, uint64(len(utf16Runes)))
|
||||
binary.Write(p.writer, binary.BigEndian, utf16Runes)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.writeCountedTag(bpTagASCIIString, uint64(len(str)))
|
||||
binary.Write(p.writer, binary.BigEndian, []byte(str))
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDictionaryTag(dict *dictionary) {
|
||||
p.writeCountedTag(bpTagDictionary, uint64(dict.count))
|
||||
vals := make([]uint64, dict.count*2)
|
||||
cnt := dict.count
|
||||
for i, k := range dict.keys {
|
||||
keyIdx, ok := p.uniqmap[k]
|
||||
if !ok {
|
||||
panic(errors.New("failed to find key " + k + " in object map during serialization"))
|
||||
}
|
||||
vals[i] = keyIdx
|
||||
}
|
||||
for i, v := range dict.values {
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
vals[i+cnt] = objIdx
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
p.writeSizedInt(v, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeArrayTag(arr []*plistValue) {
|
||||
p.writeCountedTag(bpTagArray, uint64(len(arr)))
|
||||
for _, v := range arr {
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
|
||||
p.writeSizedInt(objIdx, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) Indent(i string) {
|
||||
// There's nothing to indent.
|
||||
}
|
||||
|
||||
func newBplistGenerator(w io.Writer) *bplistGenerator {
|
||||
return &bplistGenerator{
|
||||
writer: &countedWriter{Writer: mustWriter{w}},
|
||||
}
|
||||
}
|
||||
|
||||
type bplistParser struct {
|
||||
reader io.ReadSeeker
|
||||
version int
|
||||
buf []byte
|
||||
objrefs map[uint64]*plistValue
|
||||
offtable []uint64
|
||||
trailer bplistTrailer
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
if _, ok := r.(invalidPlistError); ok {
|
||||
parseError = r.(error)
|
||||
} else {
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"binary", r.(error)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
magic := make([]byte, 6)
|
||||
ver := make([]byte, 2)
|
||||
p.reader.Seek(0, 0)
|
||||
p.reader.Read(magic)
|
||||
if !bytes.Equal(magic, []byte("bplist")) {
|
||||
panic(invalidPlistError{"binary", errors.New("mismatched magic")})
|
||||
}
|
||||
|
||||
_, err := p.reader.Read(ver)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.version = int(mustParseInt(string(ver), 10, 0))
|
||||
|
||||
if p.version > 1 {
|
||||
panic(fmt.Errorf("unexpected version %d", p.version))
|
||||
}
|
||||
|
||||
p.objrefs = make(map[uint64]*plistValue)
|
||||
_, err = p.reader.Seek(-32, 2)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = binary.Read(p.reader, binary.BigEndian, &p.trailer)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.offtable = make([]uint64, p.trailer.NumObjects)
|
||||
|
||||
// SEEK_SET
|
||||
_, err = p.reader.Seek(int64(p.trailer.OffsetTableOffset), 0)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := uint64(0); i < p.trailer.NumObjects; i++ {
|
||||
off := p.readSizedInt(int(p.trailer.OffsetIntSize))
|
||||
p.offtable[i] = off
|
||||
}
|
||||
|
||||
for _, off := range p.offtable {
|
||||
p.valueAtOffset(off)
|
||||
}
|
||||
|
||||
pval = p.valueAtOffset(p.offtable[p.trailer.TopObject])
|
||||
return
|
||||
}
|
||||
|
||||
func (p *bplistParser) readSizedInt(nbytes int) uint64 {
|
||||
switch nbytes {
|
||||
case 1:
|
||||
var val uint8
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 2:
|
||||
var val uint16
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 4:
|
||||
var val uint32
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 8:
|
||||
var val uint64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return uint64(val)
|
||||
case 16:
|
||||
var high, low uint64
|
||||
binary.Read(p.reader, binary.BigEndian, &high)
|
||||
binary.Read(p.reader, binary.BigEndian, &low)
|
||||
// TODO: int128 support (!)
|
||||
return uint64(low)
|
||||
}
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
|
||||
func (p *bplistParser) countForTag(tag uint8) uint64 {
|
||||
cnt := uint64(tag & 0x0F)
|
||||
if cnt == 0xF {
|
||||
var intTag uint8
|
||||
binary.Read(p.reader, binary.BigEndian, &intTag)
|
||||
cnt = p.readSizedInt(1 << (intTag & 0xF))
|
||||
}
|
||||
return cnt
|
||||
}
|
||||
|
||||
func (p *bplistParser) valueAtOffset(off uint64) *plistValue {
|
||||
if pval, ok := p.objrefs[off]; ok {
|
||||
return pval
|
||||
}
|
||||
pval := p.parseTagAtOffset(int64(off))
|
||||
p.objrefs[off] = pval
|
||||
return pval
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseTagAtOffset(off int64) *plistValue {
|
||||
var tag uint8
|
||||
p.reader.Seek(off, 0)
|
||||
binary.Read(p.reader, binary.BigEndian, &tag)
|
||||
|
||||
switch tag & 0xF0 {
|
||||
case bpTagNull:
|
||||
switch tag & 0x0F {
|
||||
case bpTagBoolTrue, bpTagBoolFalse:
|
||||
return &plistValue{Boolean, tag == bpTagBoolTrue}
|
||||
}
|
||||
return nil
|
||||
case bpTagInteger:
|
||||
val := p.readSizedInt(1 << (tag & 0xF))
|
||||
return &plistValue{Integer, signedInt{val, false}}
|
||||
case bpTagReal:
|
||||
nbytes := 1 << (tag & 0x0F)
|
||||
switch nbytes {
|
||||
case 4:
|
||||
var val float32
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return &plistValue{Real, sizedFloat{float64(val), 32}}
|
||||
case 8:
|
||||
var val float64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
return &plistValue{Real, sizedFloat{float64(val), 64}}
|
||||
}
|
||||
panic(errors.New("illegal float size"))
|
||||
case bpTagDate:
|
||||
var val float64
|
||||
binary.Read(p.reader, binary.BigEndian, &val)
|
||||
|
||||
// Apple Epoch is 20110101000000Z
|
||||
// Adjust for UNIX Time
|
||||
val += 978307200
|
||||
|
||||
sec, fsec := math.Modf(val)
|
||||
time := time.Unix(int64(sec), int64(fsec*float64(time.Second))).In(time.UTC)
|
||||
return &plistValue{Date, time}
|
||||
case bpTagData:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
bytes := make([]byte, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
return &plistValue{Data, bytes}
|
||||
case bpTagASCIIString, bpTagUTF16String:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
if tag&0xF0 == bpTagASCIIString {
|
||||
bytes := make([]byte, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
return &plistValue{String, string(bytes)}
|
||||
} else {
|
||||
bytes := make([]uint16, cnt)
|
||||
binary.Read(p.reader, binary.BigEndian, bytes)
|
||||
runes := utf16.Decode(bytes)
|
||||
return &plistValue{String, string(runes)}
|
||||
}
|
||||
case bpTagUID: // Somehow different than int: low half is nbytes - 1 instead of log2(nbytes)
|
||||
val := p.readSizedInt(int(tag&0xF) + 1)
|
||||
return &plistValue{Integer, signedInt{val, false}}
|
||||
case bpTagDictionary:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
subvalues := make(map[string]*plistValue)
|
||||
indices := make([]uint64, cnt*2)
|
||||
for i := uint64(0); i < cnt*2; i++ {
|
||||
idx := p.readSizedInt(int(p.trailer.ObjectRefSize))
|
||||
indices[i] = idx
|
||||
}
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
kval := p.valueAtOffset(p.offtable[indices[i]])
|
||||
subvalues[kval.value.(string)] = p.valueAtOffset(p.offtable[indices[i+cnt]])
|
||||
}
|
||||
|
||||
return &plistValue{Dictionary, &dictionary{m: subvalues}}
|
||||
case bpTagArray:
|
||||
cnt := p.countForTag(tag)
|
||||
|
||||
arr := make([]*plistValue, cnt)
|
||||
indices := make([]uint64, cnt)
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
indices[i] = p.readSizedInt(int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
arr[i] = p.valueAtOffset(p.offtable[indices[i]])
|
||||
}
|
||||
|
||||
return &plistValue{Array, arr}
|
||||
}
|
||||
panic(fmt.Errorf("unexpected atom 0x%2.02x at offset %d", tag, off))
|
||||
}
|
||||
|
||||
func newBplistParser(r io.ReadSeeker) *bplistParser {
|
||||
return &bplistParser{reader: r}
|
||||
}
|
||||
|
||||
303
vendor/github.com/DHowett/go-plist/bplist_generator.go
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
func bplistMinimumIntSize(n uint64) int {
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
return 1
|
||||
case n <= uint64(0xffff):
|
||||
return 2
|
||||
case n <= uint64(0xffffffff):
|
||||
return 4
|
||||
default:
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
func bplistValueShouldUnique(pval cfValue) bool {
|
||||
switch pval.(type) {
|
||||
case cfString, *cfNumber, *cfReal, cfDate, cfData:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type bplistGenerator struct {
|
||||
writer *countedWriter
|
||||
objmap map[interface{}]uint64 // maps pValue.hash()es to object locations
|
||||
objtable []cfValue
|
||||
trailer bplistTrailer
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) flattenPlistValue(pval cfValue) {
|
||||
key := pval.hash()
|
||||
if bplistValueShouldUnique(pval) {
|
||||
if _, ok := p.objmap[key]; ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.objmap[key] = uint64(len(p.objtable))
|
||||
p.objtable = append(p.objtable, pval)
|
||||
|
||||
switch pval := pval.(type) {
|
||||
case *cfDictionary:
|
||||
pval.sort()
|
||||
for _, k := range pval.keys {
|
||||
p.flattenPlistValue(cfString(k))
|
||||
}
|
||||
for _, v := range pval.values {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
case *cfArray:
|
||||
for _, v := range pval.values {
|
||||
p.flattenPlistValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) indexForPlistValue(pval cfValue) (uint64, bool) {
|
||||
v, ok := p.objmap[pval.hash()]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) generateDocument(root cfValue) {
|
||||
p.objtable = make([]cfValue, 0, 16)
|
||||
p.objmap = make(map[interface{}]uint64)
|
||||
p.flattenPlistValue(root)
|
||||
|
||||
p.trailer.NumObjects = uint64(len(p.objtable))
|
||||
p.trailer.ObjectRefSize = uint8(bplistMinimumIntSize(p.trailer.NumObjects))
|
||||
|
||||
p.writer.Write([]byte("bplist00"))
|
||||
|
||||
offtable := make([]uint64, p.trailer.NumObjects)
|
||||
for i, pval := range p.objtable {
|
||||
offtable[i] = uint64(p.writer.BytesWritten())
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
p.trailer.OffsetIntSize = uint8(bplistMinimumIntSize(uint64(p.writer.BytesWritten())))
|
||||
p.trailer.TopObject = p.objmap[root.hash()]
|
||||
p.trailer.OffsetTableOffset = uint64(p.writer.BytesWritten())
|
||||
|
||||
for _, offset := range offtable {
|
||||
p.writeSizedInt(offset, int(p.trailer.OffsetIntSize))
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, p.trailer)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writePlistValue(pval cfValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval := pval.(type) {
|
||||
case *cfDictionary:
|
||||
p.writeDictionaryTag(pval)
|
||||
case *cfArray:
|
||||
p.writeArrayTag(pval.values)
|
||||
case cfString:
|
||||
p.writeStringTag(string(pval))
|
||||
case *cfNumber:
|
||||
p.writeIntTag(pval.signed, pval.value)
|
||||
case *cfReal:
|
||||
if pval.wide {
|
||||
p.writeRealTag(pval.value, 64)
|
||||
} else {
|
||||
p.writeRealTag(pval.value, 32)
|
||||
}
|
||||
case cfBoolean:
|
||||
p.writeBoolTag(bool(pval))
|
||||
case cfData:
|
||||
p.writeDataTag([]byte(pval))
|
||||
case cfDate:
|
||||
p.writeDateTag(time.Time(pval))
|
||||
case cfUID:
|
||||
p.writeUIDTag(UID(pval))
|
||||
default:
|
||||
panic(fmt.Errorf("unknown plist type %t", pval))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeSizedInt(n uint64, nbytes int) {
|
||||
var val interface{}
|
||||
switch nbytes {
|
||||
case 1:
|
||||
val = uint8(n)
|
||||
case 2:
|
||||
val = uint16(n)
|
||||
case 4:
|
||||
val = uint32(n)
|
||||
case 8:
|
||||
val = n
|
||||
default:
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeBoolTag(v bool) {
|
||||
tag := uint8(bpTagBoolFalse)
|
||||
if v {
|
||||
tag = bpTagBoolTrue
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeIntTag(signed bool, n uint64) {
|
||||
var tag uint8
|
||||
var val interface{}
|
||||
switch {
|
||||
case n <= uint64(0xff):
|
||||
val = uint8(n)
|
||||
tag = bpTagInteger | 0x0
|
||||
case n <= uint64(0xffff):
|
||||
val = uint16(n)
|
||||
tag = bpTagInteger | 0x1
|
||||
case n <= uint64(0xffffffff):
|
||||
val = uint32(n)
|
||||
tag = bpTagInteger | 0x2
|
||||
case n > uint64(0x7fffffffffffffff) && !signed:
|
||||
// 64-bit values are always *signed* in format 00.
|
||||
// Any unsigned value that doesn't intersect with the signed
|
||||
// range must be sign-extended and stored as a SInt128
|
||||
val = n
|
||||
tag = bpTagInteger | 0x4
|
||||
default:
|
||||
val = n
|
||||
tag = bpTagInteger | 0x3
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
if tag&0xF == 0x4 {
|
||||
// SInt128; in the absence of true 128-bit integers in Go,
|
||||
// we'll just fake the top half. We only got here because
|
||||
// we had an unsigned 64-bit int that didn't fit,
|
||||
// so sign extend it with zeroes.
|
||||
binary.Write(p.writer, binary.BigEndian, uint64(0))
|
||||
}
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeUIDTag(u UID) {
|
||||
nbytes := bplistMinimumIntSize(uint64(u))
|
||||
tag := uint8(bpTagUID | (nbytes - 1))
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
p.writeSizedInt(uint64(u), nbytes)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeRealTag(n float64, bits int) {
|
||||
var tag uint8 = bpTagReal | 0x3
|
||||
var val interface{} = n
|
||||
if bits == 32 {
|
||||
val = float32(n)
|
||||
tag = bpTagReal | 0x2
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDateTag(t time.Time) {
|
||||
tag := uint8(bpTagDate) | 0x3
|
||||
val := float64(t.In(time.UTC).UnixNano()) / float64(time.Second)
|
||||
val -= 978307200 // Adjust to Apple Epoch
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, tag)
|
||||
binary.Write(p.writer, binary.BigEndian, val)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeCountedTag(tag uint8, count uint64) {
|
||||
marker := tag
|
||||
if count >= 0xF {
|
||||
marker |= 0xF
|
||||
} else {
|
||||
marker |= uint8(count)
|
||||
}
|
||||
|
||||
binary.Write(p.writer, binary.BigEndian, marker)
|
||||
|
||||
if count >= 0xF {
|
||||
p.writeIntTag(false, count)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDataTag(data []byte) {
|
||||
p.writeCountedTag(bpTagData, uint64(len(data)))
|
||||
binary.Write(p.writer, binary.BigEndian, data)
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeStringTag(str string) {
|
||||
for _, r := range str {
|
||||
if r > 0x7F {
|
||||
utf16Runes := utf16.Encode([]rune(str))
|
||||
p.writeCountedTag(bpTagUTF16String, uint64(len(utf16Runes)))
|
||||
binary.Write(p.writer, binary.BigEndian, utf16Runes)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.writeCountedTag(bpTagASCIIString, uint64(len(str)))
|
||||
binary.Write(p.writer, binary.BigEndian, []byte(str))
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeDictionaryTag(dict *cfDictionary) {
|
||||
// assumption: sorted already; flattenPlistValue did this.
|
||||
cnt := len(dict.keys)
|
||||
p.writeCountedTag(bpTagDictionary, uint64(cnt))
|
||||
vals := make([]uint64, cnt*2)
|
||||
for i, k := range dict.keys {
|
||||
// invariant: keys have already been "uniqued" (as PStrings)
|
||||
keyIdx, ok := p.objmap[cfString(k).hash()]
|
||||
if !ok {
|
||||
panic(errors.New("failed to find key " + k + " in object map during serialization"))
|
||||
}
|
||||
vals[i] = keyIdx
|
||||
}
|
||||
|
||||
for i, v := range dict.values {
|
||||
// invariant: values have already been "uniqued"
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
vals[i+cnt] = objIdx
|
||||
}
|
||||
|
||||
for _, v := range vals {
|
||||
p.writeSizedInt(v, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) writeArrayTag(arr []cfValue) {
|
||||
p.writeCountedTag(bpTagArray, uint64(len(arr)))
|
||||
for _, v := range arr {
|
||||
objIdx, ok := p.indexForPlistValue(v)
|
||||
if !ok {
|
||||
panic(errors.New("failed to find value in object map during serialization"))
|
||||
}
|
||||
|
||||
p.writeSizedInt(objIdx, int(p.trailer.ObjectRefSize))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistGenerator) Indent(i string) {
|
||||
// There's nothing to indent.
|
||||
}
|
||||
|
||||
func newBplistGenerator(w io.Writer) *bplistGenerator {
|
||||
return &bplistGenerator{
|
||||
writer: &countedWriter{Writer: mustWriter{w}},
|
||||
}
|
||||
}
|
||||
353
vendor/github.com/DHowett/go-plist/bplist_parser.go
generated
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"runtime"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
const (
|
||||
signedHighBits = 0xFFFFFFFFFFFFFFFF
|
||||
)
|
||||
|
||||
type offset uint64
|
||||
|
||||
type bplistParser struct {
|
||||
buffer []byte
|
||||
|
||||
reader io.ReadSeeker
|
||||
version int
|
||||
objects []cfValue // object ID to object
|
||||
trailer bplistTrailer
|
||||
trailerOffset uint64
|
||||
|
||||
containerStack []offset // slice of object offsets; manipulated during container deserialization
|
||||
}
|
||||
|
||||
func (p *bplistParser) validateDocumentTrailer() {
|
||||
if p.trailer.OffsetTableOffset >= p.trailerOffset {
|
||||
panic(fmt.Errorf("offset table beyond beginning of trailer (0x%x, trailer@0x%x)", p.trailer.OffsetTableOffset, p.trailerOffset))
|
||||
}
|
||||
|
||||
if p.trailer.OffsetTableOffset < 9 {
|
||||
panic(fmt.Errorf("offset table begins inside header (0x%x)", p.trailer.OffsetTableOffset))
|
||||
}
|
||||
|
||||
if p.trailerOffset > (p.trailer.NumObjects*uint64(p.trailer.OffsetIntSize))+p.trailer.OffsetTableOffset {
|
||||
panic(errors.New("garbage between offset table and trailer"))
|
||||
}
|
||||
|
||||
if p.trailer.OffsetTableOffset+(uint64(p.trailer.OffsetIntSize)*p.trailer.NumObjects) > p.trailerOffset {
|
||||
panic(errors.New("offset table isn't long enough to address every object"))
|
||||
}
|
||||
|
||||
maxObjectRef := uint64(1) << (8 * p.trailer.ObjectRefSize)
|
||||
if p.trailer.NumObjects > maxObjectRef {
|
||||
panic(fmt.Errorf("more objects (%v) than object ref size (%v bytes) can support", p.trailer.NumObjects, p.trailer.ObjectRefSize))
|
||||
}
|
||||
|
||||
if p.trailer.OffsetIntSize < uint8(8) && (uint64(1)<<(8*p.trailer.OffsetIntSize)) <= p.trailer.OffsetTableOffset {
|
||||
panic(errors.New("offset size isn't big enough to address entire file"))
|
||||
}
|
||||
|
||||
if p.trailer.TopObject >= p.trailer.NumObjects {
|
||||
panic(fmt.Errorf("top object #%d is out of range (only %d exist)", p.trailer.TopObject, p.trailer.NumObjects))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseDocument() (pval cfValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
|
||||
parseError = plistParseError{"binary", r.(error)}
|
||||
}
|
||||
}()
|
||||
|
||||
p.buffer, _ = ioutil.ReadAll(p.reader)
|
||||
|
||||
l := len(p.buffer)
|
||||
if l < 40 {
|
||||
panic(errors.New("not enough data"))
|
||||
}
|
||||
|
||||
if !bytes.Equal(p.buffer[0:6], []byte{'b', 'p', 'l', 'i', 's', 't'}) {
|
||||
panic(errors.New("incomprehensible magic"))
|
||||
}
|
||||
|
||||
p.version = int(((p.buffer[6] - '0') * 10) + (p.buffer[7] - '0'))
|
||||
|
||||
if p.version > 1 {
|
||||
panic(fmt.Errorf("unexpected version %d", p.version))
|
||||
}
|
||||
|
||||
p.trailerOffset = uint64(l - 32)
|
||||
p.trailer = bplistTrailer{
|
||||
SortVersion: p.buffer[p.trailerOffset+5],
|
||||
OffsetIntSize: p.buffer[p.trailerOffset+6],
|
||||
ObjectRefSize: p.buffer[p.trailerOffset+7],
|
||||
NumObjects: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+8:]),
|
||||
TopObject: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+16:]),
|
||||
OffsetTableOffset: binary.BigEndian.Uint64(p.buffer[p.trailerOffset+24:]),
|
||||
}
|
||||
|
||||
p.validateDocumentTrailer()
|
||||
|
||||
// INVARIANTS:
|
||||
// - Entire offset table is before trailer
|
||||
// - Offset table begins after header
|
||||
// - Offset table can address entire document
|
||||
// - Object IDs are big enough to support the number of objects in this plist
|
||||
// - Top object is in range
|
||||
|
||||
p.objects = make([]cfValue, p.trailer.NumObjects)
|
||||
|
||||
pval = p.objectAtIndex(p.trailer.TopObject)
|
||||
return
|
||||
}
|
||||
|
||||
// parseSizedInteger returns a 128-bit integer as low64, high64
|
||||
func (p *bplistParser) parseSizedInteger(off offset, nbytes int) (lo uint64, hi uint64, newOffset offset) {
|
||||
// Per comments in CoreFoundation, format version 00 requires that all
|
||||
// 1, 2 or 4-byte integers be interpreted as unsigned. 8-byte integers are
|
||||
// signed (always?) and therefore must be sign extended here.
|
||||
// negative 1, 2, or 4-byte integers are always emitted as 64-bit.
|
||||
switch nbytes {
|
||||
case 1:
|
||||
lo, hi = uint64(p.buffer[off]), 0
|
||||
case 2:
|
||||
lo, hi = uint64(binary.BigEndian.Uint16(p.buffer[off:])), 0
|
||||
case 4:
|
||||
lo, hi = uint64(binary.BigEndian.Uint32(p.buffer[off:])), 0
|
||||
case 8:
|
||||
lo = binary.BigEndian.Uint64(p.buffer[off:])
|
||||
if p.buffer[off]&0x80 != 0 {
|
||||
// sign extend if lo is signed
|
||||
hi = signedHighBits
|
||||
}
|
||||
case 16:
|
||||
lo, hi = binary.BigEndian.Uint64(p.buffer[off+8:]), binary.BigEndian.Uint64(p.buffer[off:])
|
||||
default:
|
||||
panic(errors.New("illegal integer size"))
|
||||
}
|
||||
newOffset = off + offset(nbytes)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseObjectRefAtOffset(off offset) (uint64, offset) {
|
||||
oid, _, next := p.parseSizedInteger(off, int(p.trailer.ObjectRefSize))
|
||||
return oid, next
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseOffsetAtOffset(off offset) (offset, offset) {
|
||||
parsedOffset, _, next := p.parseSizedInteger(off, int(p.trailer.OffsetIntSize))
|
||||
return offset(parsedOffset), next
|
||||
}
|
||||
|
||||
func (p *bplistParser) objectAtIndex(index uint64) cfValue {
|
||||
if index >= p.trailer.NumObjects {
|
||||
panic(fmt.Errorf("invalid object#%d (max %d)", index, p.trailer.NumObjects))
|
||||
}
|
||||
|
||||
if pval := p.objects[index]; pval != nil {
|
||||
return pval
|
||||
}
|
||||
|
||||
off, _ := p.parseOffsetAtOffset(offset(p.trailer.OffsetTableOffset + (index * uint64(p.trailer.OffsetIntSize))))
|
||||
if off > offset(p.trailer.OffsetTableOffset-1) {
|
||||
panic(fmt.Errorf("object#%d starts beyond beginning of object table (0x%x, table@0x%x)", index, off, p.trailer.OffsetTableOffset))
|
||||
}
|
||||
|
||||
pval := p.parseTagAtOffset(off)
|
||||
p.objects[index] = pval
|
||||
return pval
|
||||
|
||||
}
|
||||
|
||||
func (p *bplistParser) pushNestedObject(off offset) {
|
||||
for _, v := range p.containerStack {
|
||||
if v == off {
|
||||
p.panicNestedObject(off)
|
||||
}
|
||||
}
|
||||
p.containerStack = append(p.containerStack, off)
|
||||
}
|
||||
|
||||
func (p *bplistParser) panicNestedObject(off offset) {
|
||||
ids := ""
|
||||
for _, v := range p.containerStack {
|
||||
ids += fmt.Sprintf("0x%x > ", v)
|
||||
}
|
||||
|
||||
// %s0x%d: ids above ends with " > "
|
||||
panic(fmt.Errorf("self-referential collection@0x%x (%s0x%x) cannot be deserialized", off, ids, off))
|
||||
}
|
||||
|
||||
func (p *bplistParser) popNestedObject() {
|
||||
p.containerStack = p.containerStack[:len(p.containerStack)-1]
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseTagAtOffset(off offset) cfValue {
|
||||
tag := p.buffer[off]
|
||||
|
||||
switch tag & 0xF0 {
|
||||
case bpTagNull:
|
||||
switch tag & 0x0F {
|
||||
case bpTagBoolTrue, bpTagBoolFalse:
|
||||
return cfBoolean(tag == bpTagBoolTrue)
|
||||
}
|
||||
case bpTagInteger:
|
||||
lo, hi, _ := p.parseIntegerAtOffset(off)
|
||||
return &cfNumber{
|
||||
signed: hi == signedHighBits, // a signed integer is stored as a 128-bit integer with the top 64 bits set
|
||||
value: lo,
|
||||
}
|
||||
case bpTagReal:
|
||||
nbytes := 1 << (tag & 0x0F)
|
||||
switch nbytes {
|
||||
case 4:
|
||||
bits := binary.BigEndian.Uint32(p.buffer[off+1:])
|
||||
return &cfReal{wide: false, value: float64(math.Float32frombits(bits))}
|
||||
case 8:
|
||||
bits := binary.BigEndian.Uint64(p.buffer[off+1:])
|
||||
return &cfReal{wide: true, value: math.Float64frombits(bits)}
|
||||
}
|
||||
panic(errors.New("illegal float size"))
|
||||
case bpTagDate:
|
||||
bits := binary.BigEndian.Uint64(p.buffer[off+1:])
|
||||
val := math.Float64frombits(bits)
|
||||
|
||||
// Apple Epoch is 20110101000000Z
|
||||
// Adjust for UNIX Time
|
||||
val += 978307200
|
||||
|
||||
sec, fsec := math.Modf(val)
|
||||
time := time.Unix(int64(sec), int64(fsec*float64(time.Second))).In(time.UTC)
|
||||
return cfDate(time)
|
||||
case bpTagData:
|
||||
data := p.parseDataAtOffset(off)
|
||||
return cfData(data)
|
||||
case bpTagASCIIString:
|
||||
str := p.parseASCIIStringAtOffset(off)
|
||||
return cfString(str)
|
||||
case bpTagUTF16String:
|
||||
str := p.parseUTF16StringAtOffset(off)
|
||||
return cfString(str)
|
||||
case bpTagUID: // Somehow different than int: low half is nbytes - 1 instead of log2(nbytes)
|
||||
lo, _, _ := p.parseSizedInteger(off+1, int(tag&0xF)+1)
|
||||
return cfUID(lo)
|
||||
case bpTagDictionary:
|
||||
return p.parseDictionaryAtOffset(off)
|
||||
case bpTagArray:
|
||||
return p.parseArrayAtOffset(off)
|
||||
}
|
||||
panic(fmt.Errorf("unexpected atom 0x%2.02x at offset 0x%x", tag, off))
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseIntegerAtOffset(off offset) (uint64, uint64, offset) {
|
||||
tag := p.buffer[off]
|
||||
return p.parseSizedInteger(off+1, 1<<(tag&0xF))
|
||||
}
|
||||
|
||||
func (p *bplistParser) countForTagAtOffset(off offset) (uint64, offset) {
|
||||
tag := p.buffer[off]
|
||||
cnt := uint64(tag & 0x0F)
|
||||
if cnt == 0xF {
|
||||
cnt, _, off = p.parseIntegerAtOffset(off + 1)
|
||||
return cnt, off
|
||||
}
|
||||
return cnt, off + 1
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseDataAtOffset(off offset) []byte {
|
||||
len, start := p.countForTagAtOffset(off)
|
||||
if start+offset(len) > offset(p.trailer.OffsetTableOffset) {
|
||||
panic(fmt.Errorf("data@0x%x too long (%v bytes, max is %v)", off, len, p.trailer.OffsetTableOffset-uint64(start)))
|
||||
}
|
||||
return p.buffer[start : start+offset(len)]
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseASCIIStringAtOffset(off offset) string {
|
||||
len, start := p.countForTagAtOffset(off)
|
||||
if start+offset(len) > offset(p.trailer.OffsetTableOffset) {
|
||||
panic(fmt.Errorf("ascii string@0x%x too long (%v bytes, max is %v)", off, len, p.trailer.OffsetTableOffset-uint64(start)))
|
||||
}
|
||||
|
||||
return zeroCopy8BitString(p.buffer, int(start), int(len))
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseUTF16StringAtOffset(off offset) string {
|
||||
len, start := p.countForTagAtOffset(off)
|
||||
bytes := len * 2
|
||||
if start+offset(bytes) > offset(p.trailer.OffsetTableOffset) {
|
||||
panic(fmt.Errorf("utf16 string@0x%x too long (%v bytes, max is %v)", off, bytes, p.trailer.OffsetTableOffset-uint64(start)))
|
||||
}
|
||||
|
||||
u16s := make([]uint16, len)
|
||||
for i := offset(0); i < offset(len); i++ {
|
||||
u16s[i] = binary.BigEndian.Uint16(p.buffer[start+(i*2):])
|
||||
}
|
||||
runes := utf16.Decode(u16s)
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseObjectListAtOffset(off offset, count uint64) []cfValue {
|
||||
if off+offset(count*uint64(p.trailer.ObjectRefSize)) > offset(p.trailer.OffsetTableOffset) {
|
||||
panic(fmt.Errorf("list@0x%x length (%v) puts its end beyond the offset table at 0x%x", off, count, p.trailer.OffsetTableOffset))
|
||||
}
|
||||
objects := make([]cfValue, count)
|
||||
|
||||
next := off
|
||||
var oid uint64
|
||||
for i := uint64(0); i < count; i++ {
|
||||
oid, next = p.parseObjectRefAtOffset(next)
|
||||
objects[i] = p.objectAtIndex(oid)
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseDictionaryAtOffset(off offset) *cfDictionary {
|
||||
p.pushNestedObject(off)
|
||||
defer p.popNestedObject()
|
||||
|
||||
// a dictionary is an object list of [key key key val val val]
|
||||
cnt, start := p.countForTagAtOffset(off)
|
||||
objects := p.parseObjectListAtOffset(start, cnt*2)
|
||||
|
||||
keys := make([]string, cnt)
|
||||
for i := uint64(0); i < cnt; i++ {
|
||||
if str, ok := objects[i].(cfString); ok {
|
||||
keys[i] = string(str)
|
||||
} else {
|
||||
panic(fmt.Errorf("dictionary@0x%x contains non-string key at index %d", off, i))
|
||||
}
|
||||
}
|
||||
|
||||
return &cfDictionary{
|
||||
keys: keys,
|
||||
values: objects[cnt:],
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bplistParser) parseArrayAtOffset(off offset) *cfArray {
|
||||
p.pushNestedObject(off)
|
||||
defer p.popNestedObject()
|
||||
|
||||
// an array is just an object list
|
||||
cnt, start := p.countForTagAtOffset(off)
|
||||
return &cfArray{p.parseObjectListAtOffset(start, cnt)}
|
||||
}
|
||||
|
||||
func newBplistParser(r io.ReadSeeker) *bplistParser {
|
||||
return &bplistParser{reader: r}
|
||||
}
|
||||
5
vendor/github.com/DHowett/go-plist/decode.go
generated
vendored
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type parser interface {
|
||||
parseDocument() (*plistValue, error)
|
||||
parseDocument() (cfValue, error)
|
||||
}
|
||||
|
||||
// A Decoder reads a property list from an input stream.
|
||||
@@ -38,7 +38,7 @@ func (p *Decoder) Decode(v interface{}) (err error) {
|
||||
p.reader.Seek(0, 0)
|
||||
|
||||
var parser parser
|
||||
var pval *plistValue
|
||||
var pval cfValue
|
||||
if bytes.Equal(header, []byte("bplist")) {
|
||||
parser = newBplistParser(p.reader)
|
||||
pval, err = parser.parseDocument()
|
||||
@@ -93,6 +93,7 @@ func NewDecoder(r io.ReadSeeker) *Decoder {
|
||||
// in the interface value. If the interface value is nil, Unmarshal stores one of the following in the interface value:
|
||||
//
|
||||
// string, bool, uint64, float64
|
||||
// plist.UID for "CoreFoundation Keyed Archiver UIDs" (convertible to uint64)
|
||||
// []byte, for plist data
|
||||
// []interface{}, for plist arrays
|
||||
// map[string]interface{}, for plist dictionaries
|
||||
|
||||
2
vendor/github.com/DHowett/go-plist/encode.go
generated
vendored
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type generator interface {
|
||||
generateDocument(*plistValue)
|
||||
generateDocument(cfValue)
|
||||
Indent(string)
|
||||
}
|
||||
|
||||
|
||||
17
vendor/github.com/DHowett/go-plist/fuzz.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// +build gofuzz
|
||||
|
||||
package plist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
var obj interface{}
|
||||
if err := NewDecoder(buf).Decode(&obj); err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
106
vendor/github.com/DHowett/go-plist/marshal.go
generated
vendored
@@ -25,45 +25,76 @@ func isEmptyValue(v reflect.Value) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
plistMarshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
)
|
||||
|
||||
func (p *Encoder) marshalTextInterface(marshalable encoding.TextMarshaler) *plistValue {
|
||||
func implementsInterface(val reflect.Value, interfaceType reflect.Type) (interface{}, bool) {
|
||||
if val.CanInterface() && val.Type().Implements(interfaceType) {
|
||||
return val.Interface(), true
|
||||
}
|
||||
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(interfaceType) {
|
||||
return pv.Interface(), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (p *Encoder) marshalPlistInterface(marshalable Marshaler) cfValue {
|
||||
value, err := marshalable.MarshalPlist()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p.marshal(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
// marshalTextInterface marshals a TextMarshaler to a plist string.
|
||||
func (p *Encoder) marshalTextInterface(marshalable encoding.TextMarshaler) cfValue {
|
||||
s, err := marshalable.MarshalText()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &plistValue{String, string(s)}
|
||||
return cfString(s)
|
||||
}
|
||||
|
||||
func (p *Encoder) marshalStruct(typ reflect.Type, val reflect.Value) *plistValue {
|
||||
// marshalStruct marshals a reflected struct value to a plist dictionary
|
||||
func (p *Encoder) marshalStruct(typ reflect.Type, val reflect.Value) cfValue {
|
||||
tinfo, _ := getTypeInfo(typ)
|
||||
|
||||
dict := &dictionary{
|
||||
m: make(map[string]*plistValue, len(tinfo.fields)),
|
||||
dict := &cfDictionary{
|
||||
keys: make([]string, 0, len(tinfo.fields)),
|
||||
values: make([]cfValue, 0, len(tinfo.fields)),
|
||||
}
|
||||
for _, finfo := range tinfo.fields {
|
||||
value := finfo.value(val)
|
||||
if !value.IsValid() || finfo.omitEmpty && isEmptyValue(value) {
|
||||
continue
|
||||
}
|
||||
dict.m[finfo.name] = p.marshal(value)
|
||||
dict.keys = append(dict.keys, finfo.name)
|
||||
dict.values = append(dict.values, p.marshal(value))
|
||||
}
|
||||
|
||||
return &plistValue{Dictionary, dict}
|
||||
return dict
|
||||
}
|
||||
|
||||
func (p *Encoder) marshalTime(val reflect.Value) *plistValue {
|
||||
func (p *Encoder) marshalTime(val reflect.Value) cfValue {
|
||||
time := val.Interface().(time.Time)
|
||||
return &plistValue{Date, time}
|
||||
return cfDate(time)
|
||||
}
|
||||
|
||||
func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
func (p *Encoder) marshal(val reflect.Value) cfValue {
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if receiver, can := implementsInterface(val, plistMarshalerType); can {
|
||||
return p.marshalPlistInterface(receiver.(Marshaler))
|
||||
}
|
||||
|
||||
// time.Time implements TextMarshaler, but we need to store it in RFC3339
|
||||
if val.Type() == timeType {
|
||||
return p.marshalTime(val)
|
||||
@@ -76,14 +107,8 @@ func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
}
|
||||
|
||||
// Check for text marshaler.
|
||||
if val.CanInterface() && val.Type().Implements(textMarshalerType) {
|
||||
return p.marshalTextInterface(val.Interface().(encoding.TextMarshaler))
|
||||
}
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textMarshalerType) {
|
||||
return p.marshalTextInterface(pv.Interface().(encoding.TextMarshaler))
|
||||
}
|
||||
if receiver, can := implementsInterface(val, textMarshalerType); can {
|
||||
return p.marshalTextInterface(receiver.(encoding.TextMarshaler))
|
||||
}
|
||||
|
||||
// Descend into pointers or interfaces
|
||||
@@ -98,21 +123,27 @@ func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
if typ == uidType {
|
||||
return cfUID(val.Uint())
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.Struct {
|
||||
return p.marshalStruct(typ, val)
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.String:
|
||||
return &plistValue{String, val.String()}
|
||||
return cfString(val.String())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &plistValue{Integer, signedInt{uint64(val.Int()), true}}
|
||||
return &cfNumber{signed: true, value: uint64(val.Int())}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return &plistValue{Integer, signedInt{uint64(val.Uint()), false}}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return &plistValue{Real, sizedFloat{val.Float(), val.Type().Bits()}}
|
||||
return &cfNumber{signed: false, value: val.Uint()}
|
||||
case reflect.Float32:
|
||||
return &cfReal{wide: false, value: val.Float()}
|
||||
case reflect.Float64:
|
||||
return &cfReal{wide: true, value: val.Float()}
|
||||
case reflect.Bool:
|
||||
return &plistValue{Boolean, val.Bool()}
|
||||
return cfBoolean(val.Bool())
|
||||
case reflect.Slice, reflect.Array:
|
||||
if typ.Elem().Kind() == reflect.Uint8 {
|
||||
bytes := []byte(nil)
|
||||
@@ -122,15 +153,15 @@ func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
bytes = make([]byte, val.Len())
|
||||
reflect.Copy(reflect.ValueOf(bytes), val)
|
||||
}
|
||||
return &plistValue{Data, bytes}
|
||||
return cfData(bytes)
|
||||
} else {
|
||||
subvalues := make([]*plistValue, val.Len())
|
||||
for idx, length := 0, val.Len(); idx < length; idx++ {
|
||||
if subpval := p.marshal(val.Index(idx)); subpval != nil {
|
||||
subvalues[idx] = subpval
|
||||
values := make([]cfValue, val.Len())
|
||||
for i, length := 0, val.Len(); i < length; i++ {
|
||||
if subpval := p.marshal(val.Index(i)); subpval != nil {
|
||||
values[i] = subpval
|
||||
}
|
||||
}
|
||||
return &plistValue{Array, subvalues}
|
||||
return &cfArray{values}
|
||||
}
|
||||
case reflect.Map:
|
||||
if typ.Key().Kind() != reflect.String {
|
||||
@@ -138,17 +169,18 @@ func (p *Encoder) marshal(val reflect.Value) *plistValue {
|
||||
}
|
||||
|
||||
l := val.Len()
|
||||
dict := &dictionary{
|
||||
m: make(map[string]*plistValue, l),
|
||||
dict := &cfDictionary{
|
||||
keys: make([]string, 0, l),
|
||||
values: make([]cfValue, 0, l),
|
||||
}
|
||||
for _, keyv := range val.MapKeys() {
|
||||
if subpval := p.marshal(val.MapIndex(keyv)); subpval != nil {
|
||||
dict.m[keyv.String()] = subpval
|
||||
dict.keys = append(dict.keys, keyv.String())
|
||||
dict.values = append(dict.values, subpval)
|
||||
}
|
||||
}
|
||||
return &plistValue{Dictionary, dict}
|
||||
return dict
|
||||
default:
|
||||
panic(&unknownTypeError{typ})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
104
vendor/github.com/DHowett/go-plist/plist.go
generated
vendored
@@ -2,7 +2,6 @@ package plist
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Property list format constants
|
||||
@@ -27,85 +26,6 @@ var FormatNames = map[int]string{
|
||||
GNUStepFormat: "GNUStep",
|
||||
}
|
||||
|
||||
type plistKind uint
|
||||
|
||||
const (
|
||||
Invalid plistKind = iota
|
||||
Dictionary
|
||||
Array
|
||||
String
|
||||
Integer
|
||||
Real
|
||||
Boolean
|
||||
Data
|
||||
Date
|
||||
)
|
||||
|
||||
var plistKindNames map[plistKind]string = map[plistKind]string{
|
||||
Invalid: "invalid",
|
||||
Dictionary: "dictionary",
|
||||
Array: "array",
|
||||
String: "string",
|
||||
Integer: "integer",
|
||||
Real: "real",
|
||||
Boolean: "boolean",
|
||||
Data: "data",
|
||||
Date: "date",
|
||||
}
|
||||
|
||||
type plistValue struct {
|
||||
kind plistKind
|
||||
value interface{}
|
||||
}
|
||||
|
||||
type signedInt struct {
|
||||
value uint64
|
||||
signed bool
|
||||
}
|
||||
|
||||
type sizedFloat struct {
|
||||
value float64
|
||||
bits int
|
||||
}
|
||||
|
||||
type dictionary struct {
|
||||
count int
|
||||
m map[string]*plistValue
|
||||
keys sort.StringSlice
|
||||
values []*plistValue
|
||||
}
|
||||
|
||||
func (d *dictionary) Len() int {
|
||||
return d.count
|
||||
}
|
||||
|
||||
func (d *dictionary) Less(i, j int) bool {
|
||||
return d.keys.Less(i, j)
|
||||
}
|
||||
|
||||
func (d *dictionary) Swap(i, j int) {
|
||||
d.keys.Swap(i, j)
|
||||
d.values[i], d.values[j] = d.values[j], d.values[i]
|
||||
}
|
||||
|
||||
func (d *dictionary) populateArrays() {
|
||||
if d.count > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l := len(d.m)
|
||||
d.count = l
|
||||
d.keys = make([]string, l)
|
||||
d.values = make([]*plistValue, l)
|
||||
i := 0
|
||||
for k, v := range d.m {
|
||||
d.keys[i] = k
|
||||
d.values[i] = v
|
||||
i++
|
||||
}
|
||||
sort.Sort(d)
|
||||
}
|
||||
|
||||
type unknownTypeError struct {
|
||||
typ reflect.Type
|
||||
}
|
||||
@@ -139,3 +59,27 @@ func (e plistParseError) Error() string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// A UID represents a unique object identifier. UIDs are serialized in a manner distinct from
|
||||
// that of integers.
|
||||
//
|
||||
// UIDs cannot be serialized in OpenStepFormat or GNUStepFormat property lists.
|
||||
type UID uint64
|
||||
|
||||
// Marshaler is the interface implemented by types that can marshal themselves into valid
|
||||
// property list objects. The returned value is marshaled in place of the original value
|
||||
// implementing Marshaler
|
||||
//
|
||||
// If an error is returned by MarshalPlist, marshaling stops and the error is returned.
|
||||
type Marshaler interface {
|
||||
MarshalPlist() (interface{}, error)
|
||||
}
|
||||
|
||||
// Unmarshaler is the interface implemented by types that can unmarshal themselves from
|
||||
// property list objects. The UnmarshalPlist method receives a function that may
|
||||
// be called to unmarshal the original property list value into a field or variable.
|
||||
//
|
||||
// It is safe to call the unmarshal function more than once.
|
||||
type Unmarshaler interface {
|
||||
UnmarshalPlist(unmarshal func(interface{}) error) error
|
||||
}
|
||||
|
||||
139
vendor/github.com/DHowett/go-plist/plist_types.go
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"hash/crc32"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type cfValue interface {
|
||||
typeName() string
|
||||
hash() interface{}
|
||||
}
|
||||
|
||||
type cfDictionary struct {
|
||||
keys sort.StringSlice
|
||||
values []cfValue
|
||||
}
|
||||
|
||||
func (*cfDictionary) typeName() string {
|
||||
return "dictionary"
|
||||
}
|
||||
|
||||
func (p *cfDictionary) hash() interface{} {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *cfDictionary) Len() int {
|
||||
return len(p.keys)
|
||||
}
|
||||
|
||||
func (p *cfDictionary) Less(i, j int) bool {
|
||||
return p.keys.Less(i, j)
|
||||
}
|
||||
|
||||
func (p *cfDictionary) Swap(i, j int) {
|
||||
p.keys.Swap(i, j)
|
||||
p.values[i], p.values[j] = p.values[j], p.values[i]
|
||||
}
|
||||
|
||||
func (p *cfDictionary) sort() {
|
||||
sort.Sort(p)
|
||||
}
|
||||
|
||||
type cfArray struct {
|
||||
values []cfValue
|
||||
}
|
||||
|
||||
func (*cfArray) typeName() string {
|
||||
return "array"
|
||||
}
|
||||
|
||||
func (p *cfArray) hash() interface{} {
|
||||
return p
|
||||
}
|
||||
|
||||
type cfString string
|
||||
|
||||
func (cfString) typeName() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (p cfString) hash() interface{} {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
type cfNumber struct {
|
||||
signed bool
|
||||
value uint64
|
||||
}
|
||||
|
||||
func (*cfNumber) typeName() string {
|
||||
return "integer"
|
||||
}
|
||||
|
||||
func (p *cfNumber) hash() interface{} {
|
||||
if p.signed {
|
||||
return int64(p.value)
|
||||
}
|
||||
return p.value
|
||||
}
|
||||
|
||||
type cfReal struct {
|
||||
wide bool
|
||||
value float64
|
||||
}
|
||||
|
||||
func (cfReal) typeName() string {
|
||||
return "real"
|
||||
}
|
||||
|
||||
func (p *cfReal) hash() interface{} {
|
||||
if p.wide {
|
||||
return p.value
|
||||
}
|
||||
return float32(p.value)
|
||||
}
|
||||
|
||||
type cfBoolean bool
|
||||
|
||||
func (cfBoolean) typeName() string {
|
||||
return "boolean"
|
||||
}
|
||||
|
||||
func (p cfBoolean) hash() interface{} {
|
||||
return bool(p)
|
||||
}
|
||||
|
||||
type cfUID UID
|
||||
|
||||
func (cfUID) typeName() string {
|
||||
return "UID"
|
||||
}
|
||||
|
||||
func (p cfUID) hash() interface{} {
|
||||
return p
|
||||
}
|
||||
|
||||
type cfData []byte
|
||||
|
||||
func (cfData) typeName() string {
|
||||
return "data"
|
||||
}
|
||||
|
||||
func (p cfData) hash() interface{} {
|
||||
// Data are uniqued by their checksums.
|
||||
// Todo: Look at calculating this only once and storing it somewhere;
|
||||
// crc32 is fairly quick, however.
|
||||
return crc32.ChecksumIEEE([]byte(p))
|
||||
}
|
||||
|
||||
type cfDate time.Time
|
||||
|
||||
func (cfDate) typeName() string {
|
||||
return "date"
|
||||
}
|
||||
|
||||
func (p cfDate) hash() interface{} {
|
||||
return time.Time(p)
|
||||
}
|
||||
565
vendor/github.com/DHowett/go-plist/text.go
generated
vendored
@@ -1,565 +0,0 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type textPlistGenerator struct {
|
||||
writer io.Writer
|
||||
format int
|
||||
|
||||
quotableTable *[4]uint64
|
||||
|
||||
indent string
|
||||
depth int
|
||||
|
||||
dictKvDelimiter, dictEntryDelimiter, arrayDelimiter []byte
|
||||
}
|
||||
|
||||
var (
|
||||
textPlistTimeLayout = "2006-01-02 15:04:05 -0700"
|
||||
padding = "0000"
|
||||
)
|
||||
|
||||
func (p *textPlistGenerator) generateDocument(pval *plistValue) {
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) plistQuotedString(str string) string {
|
||||
if str == "" {
|
||||
return `""`
|
||||
}
|
||||
s := ""
|
||||
quot := false
|
||||
for _, r := range str {
|
||||
if r > 0xFF {
|
||||
quot = true
|
||||
s += `\U`
|
||||
us := strconv.FormatInt(int64(r), 16)
|
||||
s += padding[len(us):]
|
||||
s += us
|
||||
} else if r > 0x7F {
|
||||
quot = true
|
||||
s += `\`
|
||||
us := strconv.FormatInt(int64(r), 8)
|
||||
s += padding[1+len(us):]
|
||||
s += us
|
||||
} else {
|
||||
c := uint8(r)
|
||||
if (*p.quotableTable)[c/64]&(1<<(c%64)) > 0 {
|
||||
quot = true
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '\a':
|
||||
s += `\a`
|
||||
case '\b':
|
||||
s += `\b`
|
||||
case '\v':
|
||||
s += `\v`
|
||||
case '\f':
|
||||
s += `\f`
|
||||
case '\\':
|
||||
s += `\\`
|
||||
case '"':
|
||||
s += `\"`
|
||||
case '\t', '\r', '\n':
|
||||
fallthrough
|
||||
default:
|
||||
s += string(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if quot {
|
||||
s = `"` + s + `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) deltaIndent(depthDelta int) {
|
||||
if depthDelta < 0 {
|
||||
p.depth--
|
||||
} else if depthDelta > 0 {
|
||||
p.depth++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writeIndent() {
|
||||
if len(p.indent) == 0 {
|
||||
return
|
||||
}
|
||||
if len(p.indent) > 0 {
|
||||
p.writer.Write([]byte("\n"))
|
||||
for i := 0; i < p.depth; i++ {
|
||||
io.WriteString(p.writer, p.indent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
p.writer.Write([]byte(`{`))
|
||||
p.deltaIndent(1)
|
||||
dict := pval.value.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for i, k := range dict.keys {
|
||||
p.writeIndent()
|
||||
io.WriteString(p.writer, p.plistQuotedString(k))
|
||||
p.writer.Write(p.dictKvDelimiter)
|
||||
p.writePlistValue(dict.values[i])
|
||||
p.writer.Write(p.dictEntryDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`}`))
|
||||
case Array:
|
||||
p.writer.Write([]byte(`(`))
|
||||
p.deltaIndent(1)
|
||||
values := pval.value.([]*plistValue)
|
||||
for _, v := range values {
|
||||
p.writeIndent()
|
||||
p.writePlistValue(v)
|
||||
p.writer.Write(p.arrayDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`)`))
|
||||
case String:
|
||||
io.WriteString(p.writer, p.plistQuotedString(pval.value.(string)))
|
||||
case Integer:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*I`))
|
||||
}
|
||||
if pval.value.(signedInt).signed {
|
||||
io.WriteString(p.writer, strconv.FormatInt(int64(pval.value.(signedInt).value), 10))
|
||||
} else {
|
||||
io.WriteString(p.writer, strconv.FormatUint(pval.value.(signedInt).value, 10))
|
||||
}
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case Real:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*R`))
|
||||
}
|
||||
io.WriteString(p.writer, strconv.FormatFloat(pval.value.(sizedFloat).value, 'g', -1, 64))
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case Boolean:
|
||||
b := pval.value.(bool)
|
||||
if p.format == GNUStepFormat {
|
||||
if b {
|
||||
p.writer.Write([]byte(`<*BY>`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`<*BN>`))
|
||||
}
|
||||
} else {
|
||||
if b {
|
||||
p.writer.Write([]byte(`1`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`0`))
|
||||
}
|
||||
}
|
||||
case Data:
|
||||
b := pval.value.([]byte)
|
||||
var hexencoded [9]byte
|
||||
var l int
|
||||
var asc = 9
|
||||
hexencoded[8] = ' '
|
||||
|
||||
p.writer.Write([]byte(`<`))
|
||||
for i := 0; i < len(b); i += 4 {
|
||||
l = i + 4
|
||||
if l >= len(b) {
|
||||
l = len(b)
|
||||
// We no longer need the space - or the rest of the buffer.
|
||||
// (we used >= above to get this part without another conditional :P)
|
||||
asc = (l - i) * 2
|
||||
}
|
||||
// Fill the buffer (only up to 8 characters, to preserve the space we implicitly include
|
||||
// at the end of every encode)
|
||||
hex.Encode(hexencoded[:8], b[i:l])
|
||||
io.WriteString(p.writer, string(hexencoded[:asc]))
|
||||
}
|
||||
p.writer.Write([]byte(`>`))
|
||||
case Date:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*D`))
|
||||
io.WriteString(p.writer, pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout))
|
||||
p.writer.Write([]byte(`>`))
|
||||
} else {
|
||||
io.WriteString(p.writer, p.plistQuotedString(pval.value.(time.Time).In(time.UTC).Format(textPlistTimeLayout)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) Indent(i string) {
|
||||
p.indent = i
|
||||
if i == "" {
|
||||
p.dictKvDelimiter = []byte(`=`)
|
||||
} else {
|
||||
// For pretty-printing
|
||||
p.dictKvDelimiter = []byte(` = `)
|
||||
}
|
||||
}
|
||||
|
||||
func newTextPlistGenerator(w io.Writer, format int) *textPlistGenerator {
|
||||
table := &osQuotable
|
||||
if format == GNUStepFormat {
|
||||
table = &gsQuotable
|
||||
}
|
||||
return &textPlistGenerator{
|
||||
writer: mustWriter{w},
|
||||
format: format,
|
||||
quotableTable: table,
|
||||
dictKvDelimiter: []byte(`=`),
|
||||
arrayDelimiter: []byte(`,`),
|
||||
dictEntryDelimiter: []byte(`;`),
|
||||
}
|
||||
}
|
||||
|
||||
type byteReader interface {
|
||||
io.Reader
|
||||
io.ByteScanner
|
||||
Peek(n int) ([]byte, error)
|
||||
ReadBytes(delim byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type textPlistParser struct {
|
||||
reader byteReader
|
||||
whitespaceReplacer *strings.Replacer
|
||||
format int
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
if _, ok := r.(invalidPlistError); ok {
|
||||
parseError = r.(error)
|
||||
} else {
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"text", r.(error)}
|
||||
}
|
||||
}
|
||||
}()
|
||||
pval = p.parsePlistValue()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *textPlistParser) chugWhitespace() {
|
||||
ws:
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
if whitespace[c/64]&(1<<(c%64)) == 0 {
|
||||
if c == '/' && err != io.EOF {
|
||||
// A / at the end of the file is not the begining of a comment.
|
||||
cs, err := p.reader.Peek(1)
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
c = cs[0]
|
||||
switch c {
|
||||
case '/':
|
||||
for {
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
} else if err == io.EOF {
|
||||
break
|
||||
}
|
||||
// TODO: UTF-8
|
||||
if c == '\n' || c == '\r' {
|
||||
break
|
||||
}
|
||||
}
|
||||
case '*':
|
||||
// Peek returned a value here, so it is safe to read.
|
||||
_, _ = p.reader.ReadByte()
|
||||
star := false
|
||||
for {
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if c == '*' {
|
||||
star = true
|
||||
} else if c == '/' && star {
|
||||
break
|
||||
} else {
|
||||
star = false
|
||||
}
|
||||
}
|
||||
default:
|
||||
p.reader.UnreadByte() // Not the beginning of a // or /* comment
|
||||
break ws
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.reader.UnreadByte()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseQuotedString() *plistValue {
|
||||
escaping := false
|
||||
s := ""
|
||||
for {
|
||||
byt, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside a quoted string!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c := rune(byt)
|
||||
if !escaping {
|
||||
if c == '"' {
|
||||
break
|
||||
} else if c == '\\' {
|
||||
escaping = true
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
escaping = false
|
||||
// Everything that is not listed here passes through unharmed.
|
||||
switch c {
|
||||
case 'a':
|
||||
c = '\a'
|
||||
case 'b':
|
||||
c = '\b'
|
||||
case 'v':
|
||||
c = '\v'
|
||||
case 'f':
|
||||
c = '\f'
|
||||
case 't':
|
||||
c = '\t'
|
||||
case 'r':
|
||||
c = '\r'
|
||||
case 'n':
|
||||
c = '\n'
|
||||
case 'x', 'u', 'U': // hex and unicode
|
||||
l := 4
|
||||
if c == 'x' {
|
||||
l = 2
|
||||
}
|
||||
hex := make([]byte, l)
|
||||
p.reader.Read(hex)
|
||||
newc := mustParseInt(string(hex), 16, 16)
|
||||
c = rune(newc)
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7': // octal!
|
||||
oct := make([]byte, 3)
|
||||
oct[0] = uint8(c)
|
||||
p.reader.Read(oct[1:])
|
||||
newc := mustParseInt(string(oct), 8, 16)
|
||||
c = rune(newc)
|
||||
}
|
||||
}
|
||||
s += string(c)
|
||||
}
|
||||
return &plistValue{String, s}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseUnquotedString() *plistValue {
|
||||
s := ""
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
// if we encounter a character that must be quoted, we're done.
|
||||
// the GNUStep quote table is more lax here, so we use it instead of the OpenStep one.
|
||||
if gsQuotable[c/64]&(1<<(c%64)) > 0 {
|
||||
p.reader.UnreadByte()
|
||||
break
|
||||
}
|
||||
s += string(c)
|
||||
}
|
||||
return &plistValue{String, s}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseDictionary() *plistValue {
|
||||
var keypv *plistValue
|
||||
subval := make(map[string]*plistValue)
|
||||
for {
|
||||
p.chugWhitespace()
|
||||
|
||||
c, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside a dictionary!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c == '}' {
|
||||
break
|
||||
} else if c == '"' {
|
||||
keypv = p.parseQuotedString()
|
||||
} else {
|
||||
p.reader.UnreadByte() // Whoops, ate part of the string
|
||||
keypv = p.parseUnquotedString()
|
||||
}
|
||||
if keypv == nil {
|
||||
// TODO better error
|
||||
panic(errors.New("missing dictionary key"))
|
||||
}
|
||||
|
||||
p.chugWhitespace()
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c != '=' {
|
||||
panic(errors.New("missing = in dictionary"))
|
||||
}
|
||||
|
||||
// whitespace is guzzled within
|
||||
val := p.parsePlistValue()
|
||||
|
||||
p.chugWhitespace()
|
||||
c, err = p.reader.ReadByte()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c != ';' {
|
||||
panic(errors.New("missing ; in dictionary"))
|
||||
}
|
||||
|
||||
subval[keypv.value.(string)] = val
|
||||
}
|
||||
return &plistValue{Dictionary, &dictionary{m: subval}}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseArray() *plistValue {
|
||||
subval := make([]*plistValue, 0, 10)
|
||||
for {
|
||||
c, err := p.reader.ReadByte()
|
||||
// EOF here is an error: we're inside an array!
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if c == ')' {
|
||||
break
|
||||
} else if c == ',' {
|
||||
continue
|
||||
}
|
||||
|
||||
p.reader.UnreadByte()
|
||||
pval := p.parsePlistValue()
|
||||
if pval.kind == String && pval.value.(string) == "" {
|
||||
continue
|
||||
}
|
||||
subval = append(subval, pval)
|
||||
}
|
||||
return &plistValue{Array, subval}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseGNUStepValue(v []byte) *plistValue {
|
||||
if len(v) < 2 {
|
||||
panic(errors.New("invalid GNUStep extended value"))
|
||||
}
|
||||
typ := v[1]
|
||||
v = v[2:]
|
||||
switch typ {
|
||||
case 'I':
|
||||
if v[0] == '-' {
|
||||
n := mustParseInt(string(v), 10, 64)
|
||||
return &plistValue{Integer, signedInt{uint64(n), true}}
|
||||
} else {
|
||||
n := mustParseUint(string(v), 10, 64)
|
||||
return &plistValue{Integer, signedInt{n, false}}
|
||||
}
|
||||
case 'R':
|
||||
n := mustParseFloat(string(v), 64)
|
||||
return &plistValue{Real, sizedFloat{n, 64}}
|
||||
case 'B':
|
||||
b := v[0] == 'Y'
|
||||
return &plistValue{Boolean, b}
|
||||
case 'D':
|
||||
t, err := time.Parse(textPlistTimeLayout, string(v))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Date, t.In(time.UTC)}
|
||||
}
|
||||
panic(errors.New("invalid GNUStep type " + string(typ)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parsePlistValue() *plistValue {
|
||||
for {
|
||||
p.chugWhitespace()
|
||||
|
||||
c, err := p.reader.ReadByte()
|
||||
if err != nil && err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
switch c {
|
||||
case '<':
|
||||
bytes, err := p.reader.ReadBytes('>')
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bytes = bytes[:len(bytes)-1]
|
||||
|
||||
if bytes[0] == '*' {
|
||||
p.format = GNUStepFormat
|
||||
return p.parseGNUStepValue(bytes)
|
||||
} else {
|
||||
s := p.whitespaceReplacer.Replace(string(bytes))
|
||||
data, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &plistValue{Data, data}
|
||||
}
|
||||
case '"':
|
||||
return p.parseQuotedString()
|
||||
case '{':
|
||||
return p.parseDictionary()
|
||||
case '(':
|
||||
return p.parseArray()
|
||||
default:
|
||||
p.reader.UnreadByte() // Place back in buffer for parseUnquotedString
|
||||
return p.parseUnquotedString()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTextPlistParser(r io.Reader) *textPlistParser {
|
||||
var reader byteReader
|
||||
if rd, ok := r.(byteReader); ok {
|
||||
reader = rd
|
||||
} else {
|
||||
reader = bufio.NewReader(r)
|
||||
}
|
||||
return &textPlistParser{
|
||||
reader: reader,
|
||||
whitespaceReplacer: strings.NewReplacer("\t", "", "\n", "", " ", "", "\r", ""),
|
||||
format: OpenStepFormat,
|
||||
}
|
||||
}
|
||||
226
vendor/github.com/DHowett/go-plist/text_generator.go
generated
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type textPlistGenerator struct {
|
||||
writer io.Writer
|
||||
format int
|
||||
|
||||
quotableTable *characterSet
|
||||
|
||||
indent string
|
||||
depth int
|
||||
|
||||
dictKvDelimiter, dictEntryDelimiter, arrayDelimiter []byte
|
||||
}
|
||||
|
||||
var (
|
||||
textPlistTimeLayout = "2006-01-02 15:04:05 -0700"
|
||||
padding = "0000"
|
||||
)
|
||||
|
||||
func (p *textPlistGenerator) generateDocument(pval cfValue) {
|
||||
p.writePlistValue(pval)
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) plistQuotedString(str string) string {
|
||||
if str == "" {
|
||||
return `""`
|
||||
}
|
||||
s := ""
|
||||
quot := false
|
||||
for _, r := range str {
|
||||
if r > 0xFF {
|
||||
quot = true
|
||||
s += `\U`
|
||||
us := strconv.FormatInt(int64(r), 16)
|
||||
s += padding[len(us):]
|
||||
s += us
|
||||
} else if r > 0x7F {
|
||||
quot = true
|
||||
s += `\`
|
||||
us := strconv.FormatInt(int64(r), 8)
|
||||
s += padding[1+len(us):]
|
||||
s += us
|
||||
} else {
|
||||
c := uint8(r)
|
||||
if p.quotableTable.ContainsByte(c) {
|
||||
quot = true
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '\a':
|
||||
s += `\a`
|
||||
case '\b':
|
||||
s += `\b`
|
||||
case '\v':
|
||||
s += `\v`
|
||||
case '\f':
|
||||
s += `\f`
|
||||
case '\\':
|
||||
s += `\\`
|
||||
case '"':
|
||||
s += `\"`
|
||||
case '\t', '\r', '\n':
|
||||
fallthrough
|
||||
default:
|
||||
s += string(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if quot {
|
||||
s = `"` + s + `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) deltaIndent(depthDelta int) {
|
||||
if depthDelta < 0 {
|
||||
p.depth--
|
||||
} else if depthDelta > 0 {
|
||||
p.depth++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writeIndent() {
|
||||
if len(p.indent) == 0 {
|
||||
return
|
||||
}
|
||||
if len(p.indent) > 0 {
|
||||
p.writer.Write([]byte("\n"))
|
||||
for i := 0; i < p.depth; i++ {
|
||||
io.WriteString(p.writer, p.indent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) writePlistValue(pval cfValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval := pval.(type) {
|
||||
case *cfDictionary:
|
||||
pval.sort()
|
||||
p.writer.Write([]byte(`{`))
|
||||
p.deltaIndent(1)
|
||||
for i, k := range pval.keys {
|
||||
p.writeIndent()
|
||||
io.WriteString(p.writer, p.plistQuotedString(k))
|
||||
p.writer.Write(p.dictKvDelimiter)
|
||||
p.writePlistValue(pval.values[i])
|
||||
p.writer.Write(p.dictEntryDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`}`))
|
||||
case *cfArray:
|
||||
p.writer.Write([]byte(`(`))
|
||||
p.deltaIndent(1)
|
||||
for _, v := range pval.values {
|
||||
p.writeIndent()
|
||||
p.writePlistValue(v)
|
||||
p.writer.Write(p.arrayDelimiter)
|
||||
}
|
||||
p.deltaIndent(-1)
|
||||
p.writeIndent()
|
||||
p.writer.Write([]byte(`)`))
|
||||
case cfString:
|
||||
io.WriteString(p.writer, p.plistQuotedString(string(pval)))
|
||||
case *cfNumber:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*I`))
|
||||
}
|
||||
if pval.signed {
|
||||
io.WriteString(p.writer, strconv.FormatInt(int64(pval.value), 10))
|
||||
} else {
|
||||
io.WriteString(p.writer, strconv.FormatUint(pval.value, 10))
|
||||
}
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case *cfReal:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*R`))
|
||||
}
|
||||
// GNUstep does not differentiate between 32/64-bit floats.
|
||||
io.WriteString(p.writer, strconv.FormatFloat(pval.value, 'g', -1, 64))
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`>`))
|
||||
}
|
||||
case cfBoolean:
|
||||
if p.format == GNUStepFormat {
|
||||
if pval {
|
||||
p.writer.Write([]byte(`<*BY>`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`<*BN>`))
|
||||
}
|
||||
} else {
|
||||
if pval {
|
||||
p.writer.Write([]byte(`1`))
|
||||
} else {
|
||||
p.writer.Write([]byte(`0`))
|
||||
}
|
||||
}
|
||||
case cfData:
|
||||
var hexencoded [9]byte
|
||||
var l int
|
||||
var asc = 9
|
||||
hexencoded[8] = ' '
|
||||
|
||||
p.writer.Write([]byte(`<`))
|
||||
b := []byte(pval)
|
||||
for i := 0; i < len(b); i += 4 {
|
||||
l = i + 4
|
||||
if l >= len(b) {
|
||||
l = len(b)
|
||||
// We no longer need the space - or the rest of the buffer.
|
||||
// (we used >= above to get this part without another conditional :P)
|
||||
asc = (l - i) * 2
|
||||
}
|
||||
// Fill the buffer (only up to 8 characters, to preserve the space we implicitly include
|
||||
// at the end of every encode)
|
||||
hex.Encode(hexencoded[:8], b[i:l])
|
||||
io.WriteString(p.writer, string(hexencoded[:asc]))
|
||||
}
|
||||
p.writer.Write([]byte(`>`))
|
||||
case cfDate:
|
||||
if p.format == GNUStepFormat {
|
||||
p.writer.Write([]byte(`<*D`))
|
||||
io.WriteString(p.writer, time.Time(pval).In(time.UTC).Format(textPlistTimeLayout))
|
||||
p.writer.Write([]byte(`>`))
|
||||
} else {
|
||||
io.WriteString(p.writer, p.plistQuotedString(time.Time(pval).In(time.UTC).Format(textPlistTimeLayout)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistGenerator) Indent(i string) {
|
||||
p.indent = i
|
||||
if i == "" {
|
||||
p.dictKvDelimiter = []byte(`=`)
|
||||
} else {
|
||||
// For pretty-printing
|
||||
p.dictKvDelimiter = []byte(` = `)
|
||||
}
|
||||
}
|
||||
|
||||
func newTextPlistGenerator(w io.Writer, format int) *textPlistGenerator {
|
||||
table := &osQuotable
|
||||
if format == GNUStepFormat {
|
||||
table = &gsQuotable
|
||||
}
|
||||
return &textPlistGenerator{
|
||||
writer: mustWriter{w},
|
||||
format: format,
|
||||
quotableTable: table,
|
||||
dictKvDelimiter: []byte(`=`),
|
||||
arrayDelimiter: []byte(`,`),
|
||||
dictEntryDelimiter: []byte(`;`),
|
||||
}
|
||||
}
|
||||
515
vendor/github.com/DHowett/go-plist/text_parser.go
generated
vendored
Normal file
@@ -0,0 +1,515 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type textPlistParser struct {
|
||||
reader io.Reader
|
||||
format int
|
||||
|
||||
input string
|
||||
start int
|
||||
pos int
|
||||
width int
|
||||
}
|
||||
|
||||
func convertU16(buffer []byte, bo binary.ByteOrder) (string, error) {
|
||||
if len(buffer)%2 != 0 {
|
||||
return "", errors.New("truncated utf16")
|
||||
}
|
||||
|
||||
tmp := make([]uint16, len(buffer)/2)
|
||||
for i := 0; i < len(buffer); i += 2 {
|
||||
tmp[i/2] = bo.Uint16(buffer[i : i+2])
|
||||
}
|
||||
return string(utf16.Decode(tmp)), nil
|
||||
}
|
||||
|
||||
func guessEncodingAndConvert(buffer []byte) (string, error) {
|
||||
if len(buffer) >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF {
|
||||
// UTF-8 BOM
|
||||
return zeroCopy8BitString(buffer, 3, len(buffer)-3), nil
|
||||
} else if len(buffer) >= 2 {
|
||||
// UTF-16 guesses
|
||||
|
||||
switch {
|
||||
// stream is big-endian (BOM is FE FF or head is 00 XX)
|
||||
case (buffer[0] == 0xFE && buffer[1] == 0xFF):
|
||||
return convertU16(buffer[2:], binary.BigEndian)
|
||||
case (buffer[0] == 0 && buffer[1] != 0):
|
||||
return convertU16(buffer, binary.BigEndian)
|
||||
|
||||
// stream is little-endian (BOM is FE FF or head is XX 00)
|
||||
case (buffer[0] == 0xFF && buffer[1] == 0xFE):
|
||||
return convertU16(buffer[2:], binary.LittleEndian)
|
||||
case (buffer[0] != 0 && buffer[1] == 0):
|
||||
return convertU16(buffer, binary.LittleEndian)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: assume ASCII (not great!)
|
||||
return zeroCopy8BitString(buffer, 0, len(buffer)), nil
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseDocument() (pval cfValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
// Wrap all non-invalid-plist errors.
|
||||
parseError = plistParseError{"text", r.(error)}
|
||||
}
|
||||
}()
|
||||
|
||||
buffer, err := ioutil.ReadAll(p.reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.input, err = guessEncodingAndConvert(buffer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
val := p.parsePlistValue()
|
||||
|
||||
p.skipWhitespaceAndComments()
|
||||
if p.peek() != eof {
|
||||
if _, ok := val.(cfString); !ok {
|
||||
p.error("garbage after end of document")
|
||||
}
|
||||
|
||||
p.start = 0
|
||||
p.pos = 0
|
||||
val = p.parseDictionary(true)
|
||||
}
|
||||
|
||||
pval = val
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const eof rune = -1
|
||||
|
||||
func (p *textPlistParser) error(e string, args ...interface{}) {
|
||||
line := strings.Count(p.input[:p.pos], "\n")
|
||||
char := p.pos - strings.LastIndex(p.input[:p.pos], "\n") - 1
|
||||
panic(fmt.Errorf("%s at line %d character %d", fmt.Sprintf(e, args...), line, char))
|
||||
}
|
||||
|
||||
func (p *textPlistParser) next() rune {
|
||||
if int(p.pos) >= len(p.input) {
|
||||
p.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
p.width = w
|
||||
p.pos += p.width
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *textPlistParser) backup() {
|
||||
p.pos -= p.width
|
||||
}
|
||||
|
||||
func (p *textPlistParser) peek() rune {
|
||||
r := p.next()
|
||||
p.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *textPlistParser) emit() string {
|
||||
s := p.input[p.start:p.pos]
|
||||
p.start = p.pos
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *textPlistParser) ignore() {
|
||||
p.start = p.pos
|
||||
}
|
||||
|
||||
func (p *textPlistParser) empty() bool {
|
||||
return p.start == p.pos
|
||||
}
|
||||
|
||||
func (p *textPlistParser) scanUntil(ch rune) {
|
||||
if x := strings.IndexRune(p.input[p.pos:], ch); x >= 0 {
|
||||
p.pos += x
|
||||
return
|
||||
}
|
||||
p.pos = len(p.input)
|
||||
}
|
||||
|
||||
func (p *textPlistParser) scanUntilAny(chs string) {
|
||||
if x := strings.IndexAny(p.input[p.pos:], chs); x >= 0 {
|
||||
p.pos += x
|
||||
return
|
||||
}
|
||||
p.pos = len(p.input)
|
||||
}
|
||||
|
||||
func (p *textPlistParser) scanCharactersInSet(ch *characterSet) {
|
||||
for ch.Contains(p.next()) {
|
||||
}
|
||||
p.backup()
|
||||
}
|
||||
|
||||
func (p *textPlistParser) scanCharactersNotInSet(ch *characterSet) {
|
||||
var r rune
|
||||
for {
|
||||
r = p.next()
|
||||
if r == eof || ch.Contains(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.backup()
|
||||
}
|
||||
|
||||
func (p *textPlistParser) skipWhitespaceAndComments() {
|
||||
for {
|
||||
p.scanCharactersInSet(&whitespace)
|
||||
if strings.HasPrefix(p.input[p.pos:], "//") {
|
||||
p.scanCharactersNotInSet(&newlineCharacterSet)
|
||||
} else if strings.HasPrefix(p.input[p.pos:], "/*") {
|
||||
if x := strings.Index(p.input[p.pos:], "*/"); x >= 0 {
|
||||
p.pos += x + 2 // skip the */ as well
|
||||
continue // consume more whitespace
|
||||
} else {
|
||||
p.error("unexpected eof in block comment")
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.ignore()
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseOctalDigits(max int) uint64 {
|
||||
var val uint64
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
r := p.next()
|
||||
|
||||
if r >= '0' && r <= '7' {
|
||||
val <<= 3
|
||||
val |= uint64((r - '0'))
|
||||
} else {
|
||||
p.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseHexDigits(max int) uint64 {
|
||||
var val uint64
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
r := p.next()
|
||||
|
||||
if r >= 'a' && r <= 'f' {
|
||||
val <<= 4
|
||||
val |= 10 + uint64((r - 'a'))
|
||||
} else if r >= 'A' && r <= 'F' {
|
||||
val <<= 4
|
||||
val |= 10 + uint64((r - 'A'))
|
||||
} else if r >= '0' && r <= '9' {
|
||||
val <<= 4
|
||||
val |= uint64((r - '0'))
|
||||
} else {
|
||||
p.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// the \ has already been consumed
|
||||
func (p *textPlistParser) parseEscape() string {
|
||||
var s string
|
||||
switch p.next() {
|
||||
case 'a':
|
||||
s = "\a"
|
||||
case 'b':
|
||||
s = "\b"
|
||||
case 'v':
|
||||
s = "\v"
|
||||
case 'f':
|
||||
s = "\f"
|
||||
case 't':
|
||||
s = "\t"
|
||||
case 'r':
|
||||
s = "\r"
|
||||
case 'n':
|
||||
s = "\n"
|
||||
case '\\':
|
||||
s = `\`
|
||||
case '"':
|
||||
s = `"`
|
||||
case 'x':
|
||||
s = string(rune(p.parseHexDigits(2)))
|
||||
case 'u', 'U':
|
||||
s = string(rune(p.parseHexDigits(4)))
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
p.backup() // we've already consumed one of the digits
|
||||
s = string(rune(p.parseOctalDigits(3)))
|
||||
default:
|
||||
p.backup() // everything else should be accepted
|
||||
}
|
||||
p.ignore() // skip the entire escape sequence
|
||||
return s
|
||||
}
|
||||
|
||||
// the " has already been consumed
|
||||
func (p *textPlistParser) parseQuotedString() cfString {
|
||||
p.ignore() // ignore the "
|
||||
|
||||
slowPath := false
|
||||
s := ""
|
||||
|
||||
for {
|
||||
p.scanUntilAny(`"\`)
|
||||
switch p.peek() {
|
||||
case eof:
|
||||
p.error("unexpected eof in quoted string")
|
||||
case '"':
|
||||
section := p.emit()
|
||||
p.pos++ // skip "
|
||||
if !slowPath {
|
||||
return cfString(section)
|
||||
} else {
|
||||
s += section
|
||||
return cfString(s)
|
||||
}
|
||||
case '\\':
|
||||
slowPath = true
|
||||
s += p.emit()
|
||||
p.next() // consume \
|
||||
s += p.parseEscape()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parseUnquotedString() cfString {
|
||||
p.scanCharactersNotInSet(&gsQuotable)
|
||||
s := p.emit()
|
||||
if s == "" {
|
||||
p.error("invalid unquoted string (found an unquoted character that should be quoted?)")
|
||||
}
|
||||
|
||||
return cfString(s)
|
||||
}
|
||||
|
||||
// the { has already been consumed
|
||||
func (p *textPlistParser) parseDictionary(ignoreEof bool) *cfDictionary {
|
||||
//p.ignore() // ignore the {
|
||||
var keypv cfValue
|
||||
keys := make([]string, 0, 32)
|
||||
values := make([]cfValue, 0, 32)
|
||||
outer:
|
||||
for {
|
||||
p.skipWhitespaceAndComments()
|
||||
|
||||
switch p.next() {
|
||||
case eof:
|
||||
if !ignoreEof {
|
||||
p.error("unexpected eof in dictionary")
|
||||
}
|
||||
fallthrough
|
||||
case '}':
|
||||
break outer
|
||||
case '"':
|
||||
keypv = p.parseQuotedString()
|
||||
default:
|
||||
p.backup()
|
||||
keypv = p.parseUnquotedString()
|
||||
}
|
||||
|
||||
// INVARIANT: key can't be nil; parseQuoted and parseUnquoted
|
||||
// will panic out before they return nil.
|
||||
|
||||
p.skipWhitespaceAndComments()
|
||||
|
||||
var val cfValue
|
||||
n := p.next()
|
||||
if n == ';' {
|
||||
val = keypv
|
||||
} else if n == '=' {
|
||||
// whitespace is consumed within
|
||||
val = p.parsePlistValue()
|
||||
|
||||
p.skipWhitespaceAndComments()
|
||||
|
||||
if p.next() != ';' {
|
||||
p.error("missing ; in dictionary")
|
||||
}
|
||||
} else {
|
||||
p.error("missing = in dictionary")
|
||||
}
|
||||
|
||||
keys = append(keys, string(keypv.(cfString)))
|
||||
values = append(values, val)
|
||||
}
|
||||
|
||||
return &cfDictionary{keys: keys, values: values}
|
||||
}
|
||||
|
||||
// the ( has already been consumed
|
||||
func (p *textPlistParser) parseArray() *cfArray {
|
||||
//p.ignore() // ignore the (
|
||||
values := make([]cfValue, 0, 32)
|
||||
outer:
|
||||
for {
|
||||
p.skipWhitespaceAndComments()
|
||||
|
||||
switch p.next() {
|
||||
case eof:
|
||||
p.error("unexpected eof in array")
|
||||
case ')':
|
||||
break outer // done here
|
||||
case ',':
|
||||
continue // restart; ,) is valid and we don't want to blow it
|
||||
default:
|
||||
p.backup()
|
||||
}
|
||||
|
||||
pval := p.parsePlistValue() // whitespace is consumed within
|
||||
if str, ok := pval.(cfString); ok && string(str) == "" {
|
||||
// Empty strings in arrays are apparently skipped?
|
||||
// TODO: Figure out why this was implemented.
|
||||
continue
|
||||
}
|
||||
values = append(values, pval)
|
||||
}
|
||||
return &cfArray{values}
|
||||
}
|
||||
|
||||
// the <* have already been consumed
|
||||
func (p *textPlistParser) parseGNUStepValue() cfValue {
|
||||
typ := p.next()
|
||||
p.ignore()
|
||||
p.scanUntil('>')
|
||||
|
||||
if typ == eof || typ == '>' || p.empty() || p.peek() == eof {
|
||||
p.error("invalid GNUStep extended value")
|
||||
}
|
||||
|
||||
v := p.emit()
|
||||
p.next() // consume the >
|
||||
|
||||
switch typ {
|
||||
case 'I':
|
||||
if v[0] == '-' {
|
||||
n := mustParseInt(v, 10, 64)
|
||||
return &cfNumber{signed: true, value: uint64(n)}
|
||||
} else {
|
||||
n := mustParseUint(v, 10, 64)
|
||||
return &cfNumber{signed: false, value: n}
|
||||
}
|
||||
case 'R':
|
||||
n := mustParseFloat(v, 64)
|
||||
return &cfReal{wide: true, value: n} // TODO(DH) 32/64
|
||||
case 'B':
|
||||
b := v[0] == 'Y'
|
||||
return cfBoolean(b)
|
||||
case 'D':
|
||||
t, err := time.Parse(textPlistTimeLayout, v)
|
||||
if err != nil {
|
||||
p.error(err.Error())
|
||||
}
|
||||
|
||||
return cfDate(t.In(time.UTC))
|
||||
}
|
||||
p.error("invalid GNUStep type " + string(typ))
|
||||
return nil
|
||||
}
|
||||
|
||||
// The < has already been consumed
|
||||
func (p *textPlistParser) parseHexData() cfData {
|
||||
buf := make([]byte, 256)
|
||||
i := 0
|
||||
c := 0
|
||||
|
||||
for {
|
||||
r := p.next()
|
||||
switch r {
|
||||
case eof:
|
||||
p.error("unexpected eof in data")
|
||||
case '>':
|
||||
if c&1 == 1 {
|
||||
p.error("uneven number of hex digits in data")
|
||||
}
|
||||
p.ignore()
|
||||
return cfData(buf[:i])
|
||||
case ' ', '\t', '\n', '\r', '\u2028', '\u2029': // more lax than apple here: skip spaces
|
||||
continue
|
||||
}
|
||||
|
||||
buf[i] <<= 4
|
||||
if r >= 'a' && r <= 'f' {
|
||||
buf[i] |= 10 + byte((r - 'a'))
|
||||
} else if r >= 'A' && r <= 'F' {
|
||||
buf[i] |= 10 + byte((r - 'A'))
|
||||
} else if r >= '0' && r <= '9' {
|
||||
buf[i] |= byte((r - '0'))
|
||||
} else {
|
||||
p.error("unexpected hex digit `%c'", r)
|
||||
}
|
||||
|
||||
c++
|
||||
if c&1 == 0 {
|
||||
i++
|
||||
if i >= len(buf) {
|
||||
realloc := make([]byte, len(buf)*2)
|
||||
copy(realloc, buf)
|
||||
buf = realloc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textPlistParser) parsePlistValue() cfValue {
|
||||
for {
|
||||
p.skipWhitespaceAndComments()
|
||||
|
||||
switch p.next() {
|
||||
case eof:
|
||||
return &cfDictionary{}
|
||||
case '<':
|
||||
if p.next() == '*' {
|
||||
p.format = GNUStepFormat
|
||||
return p.parseGNUStepValue()
|
||||
}
|
||||
|
||||
p.backup()
|
||||
return p.parseHexData()
|
||||
case '"':
|
||||
return p.parseQuotedString()
|
||||
case '{':
|
||||
return p.parseDictionary(false)
|
||||
case '(':
|
||||
return p.parseArray()
|
||||
default:
|
||||
p.backup()
|
||||
return p.parseUnquotedString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTextPlistParser(r io.Reader) *textPlistParser {
|
||||
return &textPlistParser{
|
||||
reader: r,
|
||||
format: OpenStepFormat,
|
||||
}
|
||||
}
|
||||
23
vendor/github.com/DHowett/go-plist/text_tables.go
generated
vendored
@@ -1,9 +1,19 @@
|
||||
package plist
|
||||
|
||||
type characterSet [4]uint64
|
||||
|
||||
func (s *characterSet) Contains(ch rune) bool {
|
||||
return ch >= 0 && ch <= 255 && s.ContainsByte(byte(ch))
|
||||
}
|
||||
|
||||
func (s *characterSet) ContainsByte(ch byte) bool {
|
||||
return (s[ch/64]&(1<<(ch%64)) > 0)
|
||||
}
|
||||
|
||||
// Bitmap of characters that must be inside a quoted string
|
||||
// when written to an old-style property list
|
||||
// Low bits represent lower characters, and each uint64 represents 64 characters.
|
||||
var gsQuotable = [4]uint64{
|
||||
var gsQuotable = characterSet{
|
||||
0x78001385ffffffff,
|
||||
0xa800000138000000,
|
||||
0xffffffffffffffff,
|
||||
@@ -11,16 +21,23 @@ var gsQuotable = [4]uint64{
|
||||
}
|
||||
|
||||
// 7f instead of 3f in the top line: CFOldStylePlist.c says . is valid, but they quote it.
|
||||
var osQuotable = [4]uint64{
|
||||
var osQuotable = characterSet{
|
||||
0xf4007f6fffffffff,
|
||||
0xf8000001f8000001,
|
||||
0xffffffffffffffff,
|
||||
0xffffffffffffffff,
|
||||
}
|
||||
|
||||
var whitespace = [4]uint64{
|
||||
var whitespace = characterSet{
|
||||
0x0000000100003f00,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
}
|
||||
|
||||
var newlineCharacterSet = characterSet{
|
||||
0x0000000000002400,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
0x0000000000000000,
|
||||
}
|
||||
|
||||
205
vendor/github.com/DHowett/go-plist/unmarshal.go
generated
vendored
@@ -4,35 +4,57 @@ import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type incompatibleDecodeTypeError struct {
|
||||
typ reflect.Type
|
||||
pKind plistKind
|
||||
dest reflect.Type
|
||||
src string // type name (from cfValue)
|
||||
}
|
||||
|
||||
func (u *incompatibleDecodeTypeError) Error() string {
|
||||
return fmt.Sprintf("plist: type mismatch: tried to decode %v into value of type %v", plistKindNames[u.pKind], u.typ)
|
||||
return fmt.Sprintf("plist: type mismatch: tried to decode plist type `%v' into value of type `%v'", u.src, u.dest)
|
||||
}
|
||||
|
||||
var (
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
plistUnmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
uidType = reflect.TypeOf(UID(0))
|
||||
)
|
||||
|
||||
func isEmptyInterface(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Interface && v.NumMethod() == 0
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalTextInterface(pval *plistValue, unmarshalable encoding.TextUnmarshaler) {
|
||||
err := unmarshalable.UnmarshalText([]byte(pval.value.(string)))
|
||||
func (p *Decoder) unmarshalPlistInterface(pval cfValue, unmarshalable Unmarshaler) {
|
||||
err := unmarshalable.UnmarshalPlist(func(i interface{}) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
p.unmarshal(pval, reflect.ValueOf(i))
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalTime(pval *plistValue, val reflect.Value) {
|
||||
val.Set(reflect.ValueOf(pval.value.(time.Time)))
|
||||
func (p *Decoder) unmarshalTextInterface(pval cfString, unmarshalable encoding.TextUnmarshaler) {
|
||||
err := unmarshalable.UnmarshalText([]byte(pval))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalTime(pval cfDate, val reflect.Value) {
|
||||
val.Set(reflect.ValueOf(time.Time(pval)))
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
|
||||
@@ -64,11 +86,11 @@ func (p *Decoder) unmarshalLaxString(s string, val reflect.Value) {
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), String})
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), "string"})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshal(pval *plistValue, val reflect.Value) {
|
||||
func (p *Decoder) unmarshal(pval cfValue, val reflect.Value) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
@@ -86,86 +108,101 @@ func (p *Decoder) unmarshal(pval *plistValue, val reflect.Value) {
|
||||
return
|
||||
}
|
||||
|
||||
incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.kind}
|
||||
incompatibleTypeError := &incompatibleDecodeTypeError{val.Type(), pval.typeName()}
|
||||
|
||||
// time.Time implements TextMarshaler, but we need to parse it as RFC3339
|
||||
if pval.kind == Date {
|
||||
if date, ok := pval.(cfDate); ok {
|
||||
if val.Type() == timeType {
|
||||
p.unmarshalTime(pval, val)
|
||||
p.unmarshalTime(date, val)
|
||||
return
|
||||
}
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
|
||||
if val.CanInterface() && val.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
|
||||
p.unmarshalTextInterface(pval, val.Interface().(encoding.TextUnmarshaler))
|
||||
if receiver, can := implementsInterface(val, plistUnmarshalerType); can {
|
||||
p.unmarshalPlistInterface(pval, receiver.(Unmarshaler))
|
||||
return
|
||||
}
|
||||
|
||||
if val.CanAddr() {
|
||||
pv := val.Addr()
|
||||
if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) && val.Type() != timeType {
|
||||
p.unmarshalTextInterface(pval, pv.Interface().(encoding.TextUnmarshaler))
|
||||
if val.Type() != timeType {
|
||||
if receiver, can := implementsInterface(val, textUnmarshalerType); can {
|
||||
if str, ok := pval.(cfString); ok {
|
||||
p.unmarshalTextInterface(str, receiver.(encoding.TextUnmarshaler))
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
switch pval.kind {
|
||||
case String:
|
||||
switch pval := pval.(type) {
|
||||
case cfString:
|
||||
if val.Kind() == reflect.String {
|
||||
val.SetString(pval.value.(string))
|
||||
val.SetString(string(pval))
|
||||
return
|
||||
}
|
||||
if p.lax {
|
||||
p.unmarshalLaxString(pval.value.(string), val)
|
||||
p.unmarshalLaxString(string(pval), val)
|
||||
return
|
||||
}
|
||||
|
||||
panic(incompatibleTypeError)
|
||||
case Integer:
|
||||
case *cfNumber:
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
val.SetInt(int64(pval.value.(signedInt).value))
|
||||
val.SetInt(int64(pval.value))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
val.SetUint(pval.value.(signedInt).value)
|
||||
val.SetUint(pval.value)
|
||||
default:
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Real:
|
||||
case *cfReal:
|
||||
if val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64 {
|
||||
val.SetFloat(pval.value.(sizedFloat).value)
|
||||
// TODO: Consider warning on a downcast (storing a 64-bit value in a 32-bit reflect)
|
||||
val.SetFloat(pval.value)
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Boolean:
|
||||
case cfBoolean:
|
||||
if val.Kind() == reflect.Bool {
|
||||
val.SetBool(pval.value.(bool))
|
||||
val.SetBool(bool(pval))
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Data:
|
||||
case cfData:
|
||||
if val.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
|
||||
val.SetBytes(pval.value.([]byte))
|
||||
val.SetBytes([]byte(pval))
|
||||
} else {
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
case Array:
|
||||
case cfUID:
|
||||
if val.Type() == uidType {
|
||||
val.SetUint(uint64(pval))
|
||||
} else {
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
val.SetInt(int64(pval))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
val.SetUint(uint64(pval))
|
||||
default:
|
||||
panic(incompatibleTypeError)
|
||||
}
|
||||
}
|
||||
case *cfArray:
|
||||
p.unmarshalArray(pval, val)
|
||||
case Dictionary:
|
||||
case *cfDictionary:
|
||||
p.unmarshalDictionary(pval, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalArray(pval *plistValue, val reflect.Value) {
|
||||
subvalues := pval.value.([]*plistValue)
|
||||
|
||||
func (p *Decoder) unmarshalArray(a *cfArray, val reflect.Value) {
|
||||
var n int
|
||||
if val.Kind() == reflect.Slice {
|
||||
// Slice of element values.
|
||||
// Grow slice.
|
||||
cnt := len(subvalues) + val.Len()
|
||||
cnt := len(a.values) + val.Len()
|
||||
if cnt >= val.Cap() {
|
||||
ncap := 2 * cnt
|
||||
if ncap < 4 {
|
||||
@@ -178,22 +215,22 @@ func (p *Decoder) unmarshalArray(pval *plistValue, val reflect.Value) {
|
||||
n = val.Len()
|
||||
val.SetLen(cnt)
|
||||
} else if val.Kind() == reflect.Array {
|
||||
if len(subvalues) > val.Cap() {
|
||||
panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(subvalues), val.Cap()))
|
||||
if len(a.values) > val.Cap() {
|
||||
panic(fmt.Errorf("plist: attempted to unmarshal %d values into an array of size %d", len(a.values), val.Cap()))
|
||||
}
|
||||
} else {
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), pval.kind})
|
||||
panic(&incompatibleDecodeTypeError{val.Type(), a.typeName()})
|
||||
}
|
||||
|
||||
// Recur to read element into slice.
|
||||
for _, sval := range subvalues {
|
||||
for _, sval := range a.values {
|
||||
p.unmarshal(sval, val.Index(n))
|
||||
n++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Decoder) unmarshalDictionary(pval *plistValue, val reflect.Value) {
|
||||
func (p *Decoder) unmarshalDictionary(dict *cfDictionary, val reflect.Value) {
|
||||
typ := val.Type()
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
@@ -202,74 +239,78 @@ func (p *Decoder) unmarshalDictionary(pval *plistValue, val reflect.Value) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
subvalues := pval.value.(*dictionary).m
|
||||
entries := make(map[string]cfValue, len(dict.keys))
|
||||
for i, k := range dict.keys {
|
||||
sval := dict.values[i]
|
||||
entries[k] = sval
|
||||
}
|
||||
|
||||
for _, finfo := range tinfo.fields {
|
||||
p.unmarshal(subvalues[finfo.name], finfo.value(val))
|
||||
p.unmarshal(entries[finfo.name], finfo.value(val))
|
||||
}
|
||||
case reflect.Map:
|
||||
if val.IsNil() {
|
||||
val.Set(reflect.MakeMap(typ))
|
||||
}
|
||||
|
||||
subvalues := pval.value.(*dictionary).m
|
||||
for k, sval := range subvalues {
|
||||
for i, k := range dict.keys {
|
||||
sval := dict.values[i]
|
||||
|
||||
keyv := reflect.ValueOf(k).Convert(typ.Key())
|
||||
mapElem := val.MapIndex(keyv)
|
||||
if !mapElem.IsValid() {
|
||||
mapElem = reflect.New(typ.Elem()).Elem()
|
||||
}
|
||||
mapElem := reflect.New(typ.Elem()).Elem()
|
||||
|
||||
p.unmarshal(sval, mapElem)
|
||||
val.SetMapIndex(keyv, mapElem)
|
||||
}
|
||||
default:
|
||||
panic(&incompatibleDecodeTypeError{typ, pval.kind})
|
||||
panic(&incompatibleDecodeTypeError{typ, dict.typeName()})
|
||||
}
|
||||
}
|
||||
|
||||
/* *Interface is modelled after encoding/json */
|
||||
func (p *Decoder) valueInterface(pval *plistValue) interface{} {
|
||||
switch pval.kind {
|
||||
case String:
|
||||
return pval.value.(string)
|
||||
case Integer:
|
||||
if pval.value.(signedInt).signed {
|
||||
return int64(pval.value.(signedInt).value)
|
||||
func (p *Decoder) valueInterface(pval cfValue) interface{} {
|
||||
switch pval := pval.(type) {
|
||||
case cfString:
|
||||
return string(pval)
|
||||
case *cfNumber:
|
||||
if pval.signed {
|
||||
return int64(pval.value)
|
||||
}
|
||||
return pval.value.(signedInt).value
|
||||
case Real:
|
||||
bits := pval.value.(sizedFloat).bits
|
||||
switch bits {
|
||||
case 32:
|
||||
return float32(pval.value.(sizedFloat).value)
|
||||
case 64:
|
||||
return pval.value.(sizedFloat).value
|
||||
return pval.value
|
||||
case *cfReal:
|
||||
if pval.wide {
|
||||
return pval.value
|
||||
} else {
|
||||
return float32(pval.value)
|
||||
}
|
||||
case Boolean:
|
||||
return pval.value.(bool)
|
||||
case Array:
|
||||
return p.arrayInterface(pval.value.([]*plistValue))
|
||||
case Dictionary:
|
||||
return p.dictionaryInterface(pval.value.(*dictionary))
|
||||
case Data:
|
||||
return pval.value.([]byte)
|
||||
case Date:
|
||||
return pval.value.(time.Time)
|
||||
case cfBoolean:
|
||||
return bool(pval)
|
||||
case *cfArray:
|
||||
return p.arrayInterface(pval)
|
||||
case *cfDictionary:
|
||||
return p.dictionaryInterface(pval)
|
||||
case cfData:
|
||||
return []byte(pval)
|
||||
case cfDate:
|
||||
return time.Time(pval)
|
||||
case cfUID:
|
||||
return UID(pval)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Decoder) arrayInterface(subvalues []*plistValue) []interface{} {
|
||||
out := make([]interface{}, len(subvalues))
|
||||
for i, subv := range subvalues {
|
||||
func (p *Decoder) arrayInterface(a *cfArray) []interface{} {
|
||||
out := make([]interface{}, len(a.values))
|
||||
for i, subv := range a.values {
|
||||
out[i] = p.valueInterface(subv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *Decoder) dictionaryInterface(dict *dictionary) map[string]interface{} {
|
||||
func (p *Decoder) dictionaryInterface(dict *cfDictionary) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
for k, subv := range dict.m {
|
||||
for i, k := range dict.keys {
|
||||
subv := dict.values[i]
|
||||
out[k] = p.valueInterface(subv)
|
||||
}
|
||||
return out
|
||||
|
||||
7
vendor/github.com/DHowett/go-plist/util.go
generated
vendored
@@ -16,3 +16,10 @@ func (w *countedWriter) Write(p []byte) (int, error) {
|
||||
func (w *countedWriter) BytesWritten() int {
|
||||
return w.nbytes
|
||||
}
|
||||
|
||||
func unsignedGetBase(s string) (string, int) {
|
||||
if len(s) > 1 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X') {
|
||||
return s[2:], 16
|
||||
}
|
||||
return s, 10
|
||||
}
|
||||
|
||||
185
vendor/github.com/DHowett/go-plist/xml_generator.go
generated
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
package plist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
xmlHEADER string = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"
|
||||
xmlDOCTYPE = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">` + "\n"
|
||||
xmlArrayTag = "array"
|
||||
xmlDataTag = "data"
|
||||
xmlDateTag = "date"
|
||||
xmlDictTag = "dict"
|
||||
xmlFalseTag = "false"
|
||||
xmlIntegerTag = "integer"
|
||||
xmlKeyTag = "key"
|
||||
xmlPlistTag = "plist"
|
||||
xmlRealTag = "real"
|
||||
xmlStringTag = "string"
|
||||
xmlTrueTag = "true"
|
||||
|
||||
// magic value used in the XML encoding of UIDs
|
||||
// (stored as a dictionary mapping CF$UID->integer)
|
||||
xmlCFUIDMagic = "CF$UID"
|
||||
)
|
||||
|
||||
func formatXMLFloat(f float64) string {
|
||||
switch {
|
||||
case math.IsInf(f, 1):
|
||||
return "inf"
|
||||
case math.IsInf(f, -1):
|
||||
return "-inf"
|
||||
case math.IsNaN(f):
|
||||
return "nan"
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
type xmlPlistGenerator struct {
|
||||
*bufio.Writer
|
||||
|
||||
indent string
|
||||
depth int
|
||||
putNewline bool
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) generateDocument(root cfValue) {
|
||||
p.WriteString(xmlHEADER)
|
||||
p.WriteString(xmlDOCTYPE)
|
||||
|
||||
p.openTag(`plist version="1.0"`)
|
||||
p.writePlistValue(root)
|
||||
p.closeTag(xmlPlistTag)
|
||||
p.Flush()
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) openTag(n string) {
|
||||
p.writeIndent(1)
|
||||
p.WriteByte('<')
|
||||
p.WriteString(n)
|
||||
p.WriteByte('>')
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) closeTag(n string) {
|
||||
p.writeIndent(-1)
|
||||
p.WriteString("</")
|
||||
p.WriteString(n)
|
||||
p.WriteByte('>')
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) element(n string, v string) {
|
||||
p.writeIndent(0)
|
||||
if len(v) == 0 {
|
||||
p.WriteByte('<')
|
||||
p.WriteString(n)
|
||||
p.WriteString("/>")
|
||||
} else {
|
||||
p.WriteByte('<')
|
||||
p.WriteString(n)
|
||||
p.WriteByte('>')
|
||||
|
||||
err := xml.EscapeText(p.Writer, []byte(v))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p.WriteString("</")
|
||||
p.WriteString(n)
|
||||
p.WriteByte('>')
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writeDictionary(dict *cfDictionary) {
|
||||
dict.sort()
|
||||
p.openTag(xmlDictTag)
|
||||
for i, k := range dict.keys {
|
||||
p.element(xmlKeyTag, k)
|
||||
p.writePlistValue(dict.values[i])
|
||||
}
|
||||
p.closeTag(xmlDictTag)
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writeArray(a *cfArray) {
|
||||
p.openTag(xmlArrayTag)
|
||||
for _, v := range a.values {
|
||||
p.writePlistValue(v)
|
||||
}
|
||||
p.closeTag(xmlArrayTag)
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writePlistValue(pval cfValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch pval := pval.(type) {
|
||||
case cfString:
|
||||
p.element(xmlStringTag, string(pval))
|
||||
case *cfNumber:
|
||||
if pval.signed {
|
||||
p.element(xmlIntegerTag, strconv.FormatInt(int64(pval.value), 10))
|
||||
} else {
|
||||
p.element(xmlIntegerTag, strconv.FormatUint(pval.value, 10))
|
||||
}
|
||||
case *cfReal:
|
||||
p.element(xmlRealTag, formatXMLFloat(pval.value))
|
||||
case cfBoolean:
|
||||
if bool(pval) {
|
||||
p.element(xmlTrueTag, "")
|
||||
} else {
|
||||
p.element(xmlFalseTag, "")
|
||||
}
|
||||
case cfData:
|
||||
p.element(xmlDataTag, base64.StdEncoding.EncodeToString([]byte(pval)))
|
||||
case cfDate:
|
||||
p.element(xmlDateTag, time.Time(pval).In(time.UTC).Format(time.RFC3339))
|
||||
case *cfDictionary:
|
||||
p.writeDictionary(pval)
|
||||
case *cfArray:
|
||||
p.writeArray(pval)
|
||||
case cfUID:
|
||||
p.openTag(xmlDictTag)
|
||||
p.element(xmlKeyTag, xmlCFUIDMagic)
|
||||
p.element(xmlIntegerTag, strconv.FormatUint(uint64(pval), 10))
|
||||
p.closeTag(xmlDictTag)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writeIndent(delta int) {
|
||||
if len(p.indent) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if delta < 0 {
|
||||
p.depth--
|
||||
}
|
||||
|
||||
if p.putNewline {
|
||||
// from encoding/xml/marshal.go; it seems to be intended
|
||||
// to suppress the first newline.
|
||||
p.WriteByte('\n')
|
||||
} else {
|
||||
p.putNewline = true
|
||||
}
|
||||
for i := 0; i < p.depth; i++ {
|
||||
p.WriteString(p.indent)
|
||||
}
|
||||
if delta > 0 {
|
||||
p.depth++
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) Indent(i string) {
|
||||
p.indent = i
|
||||
}
|
||||
|
||||
func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
|
||||
return &xmlPlistGenerator{Writer: bufio.NewWriter(w)}
|
||||
}
|
||||
162
vendor/github.com/DHowett/go-plist/xml.go → vendor/github.com/DHowett/go-plist/xml_parser.go
generated
vendored
@@ -6,124 +6,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const xmlDOCTYPE = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
`
|
||||
|
||||
type xmlPlistGenerator struct {
|
||||
writer io.Writer
|
||||
xmlEncoder *xml.Encoder
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) generateDocument(pval *plistValue) {
|
||||
io.WriteString(p.writer, xml.Header)
|
||||
io.WriteString(p.writer, xmlDOCTYPE)
|
||||
|
||||
plistStartElement := xml.StartElement{
|
||||
Name: xml.Name{
|
||||
Space: "",
|
||||
Local: "plist",
|
||||
},
|
||||
Attr: []xml.Attr{{
|
||||
Name: xml.Name{
|
||||
Space: "",
|
||||
Local: "version"},
|
||||
Value: "1.0"},
|
||||
},
|
||||
}
|
||||
|
||||
p.xmlEncoder.EncodeToken(plistStartElement)
|
||||
|
||||
p.writePlistValue(pval)
|
||||
|
||||
p.xmlEncoder.EncodeToken(plistStartElement.End())
|
||||
p.xmlEncoder.Flush()
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) writePlistValue(pval *plistValue) {
|
||||
if pval == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer p.xmlEncoder.Flush()
|
||||
|
||||
key := ""
|
||||
encodedValue := pval.value
|
||||
switch pval.kind {
|
||||
case Dictionary:
|
||||
startElement := xml.StartElement{Name: xml.Name{Local: "dict"}}
|
||||
p.xmlEncoder.EncodeToken(startElement)
|
||||
dict := encodedValue.(*dictionary)
|
||||
dict.populateArrays()
|
||||
for i, k := range dict.keys {
|
||||
p.xmlEncoder.EncodeElement(k, xml.StartElement{Name: xml.Name{Local: "key"}})
|
||||
p.writePlistValue(dict.values[i])
|
||||
}
|
||||
p.xmlEncoder.EncodeToken(startElement.End())
|
||||
case Array:
|
||||
startElement := xml.StartElement{Name: xml.Name{Local: "array"}}
|
||||
p.xmlEncoder.EncodeToken(startElement)
|
||||
values := encodedValue.([]*plistValue)
|
||||
for _, v := range values {
|
||||
p.writePlistValue(v)
|
||||
}
|
||||
p.xmlEncoder.EncodeToken(startElement.End())
|
||||
case String:
|
||||
key = "string"
|
||||
case Integer:
|
||||
key = "integer"
|
||||
if pval.value.(signedInt).signed {
|
||||
encodedValue = int64(pval.value.(signedInt).value)
|
||||
} else {
|
||||
encodedValue = pval.value.(signedInt).value
|
||||
}
|
||||
case Real:
|
||||
key = "real"
|
||||
encodedValue = pval.value.(sizedFloat).value
|
||||
switch {
|
||||
case math.IsInf(pval.value.(sizedFloat).value, 1):
|
||||
encodedValue = "inf"
|
||||
case math.IsInf(pval.value.(sizedFloat).value, -1):
|
||||
encodedValue = "-inf"
|
||||
case math.IsNaN(pval.value.(sizedFloat).value):
|
||||
encodedValue = "nan"
|
||||
}
|
||||
case Boolean:
|
||||
key = "false"
|
||||
b := pval.value.(bool)
|
||||
if b {
|
||||
key = "true"
|
||||
}
|
||||
encodedValue = ""
|
||||
case Data:
|
||||
key = "data"
|
||||
encodedValue = xml.CharData(base64.StdEncoding.EncodeToString(pval.value.([]byte)))
|
||||
case Date:
|
||||
key = "date"
|
||||
encodedValue = pval.value.(time.Time).In(time.UTC).Format(time.RFC3339)
|
||||
}
|
||||
if key != "" {
|
||||
err := p.xmlEncoder.EncodeElement(encodedValue, xml.StartElement{Name: xml.Name{Local: key}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistGenerator) Indent(i string) {
|
||||
p.xmlEncoder.Indent("", i)
|
||||
}
|
||||
|
||||
func newXMLPlistGenerator(w io.Writer) *xmlPlistGenerator {
|
||||
mw := mustWriter{w}
|
||||
return &xmlPlistGenerator{mw, xml.NewEncoder(mw)}
|
||||
}
|
||||
|
||||
type xmlPlistParser struct {
|
||||
reader io.Reader
|
||||
xmlDecoder *xml.Decoder
|
||||
@@ -131,7 +18,7 @@ type xmlPlistParser struct {
|
||||
ntags int
|
||||
}
|
||||
|
||||
func (p *xmlPlistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
func (p *xmlPlistParser) parseDocument() (pval cfValue, parseError error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
@@ -162,7 +49,7 @@ func (p *xmlPlistParser) parseDocument() (pval *plistValue, parseError error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) cfValue {
|
||||
var charData xml.CharData
|
||||
switch element.Name.Local {
|
||||
case "plist":
|
||||
@@ -189,7 +76,7 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{String, string(charData)}
|
||||
return cfString(charData)
|
||||
case "integer":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
@@ -198,12 +85,18 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
}
|
||||
|
||||
s := string(charData)
|
||||
if len(s) == 0 {
|
||||
panic(errors.New("invalid empty <integer/>"))
|
||||
}
|
||||
|
||||
if s[0] == '-' {
|
||||
n := mustParseInt(string(charData), 10, 64)
|
||||
return &plistValue{Integer, signedInt{uint64(n), true}}
|
||||
s, base := unsignedGetBase(s[1:])
|
||||
n := mustParseInt("-"+s, base, 64)
|
||||
return &cfNumber{signed: true, value: uint64(n)}
|
||||
} else {
|
||||
n := mustParseUint(string(charData), 10, 64)
|
||||
return &plistValue{Integer, signedInt{n, false}}
|
||||
s, base := unsignedGetBase(s)
|
||||
n := mustParseUint(s, base, 64)
|
||||
return &cfNumber{signed: false, value: n}
|
||||
}
|
||||
case "real":
|
||||
p.ntags++
|
||||
@@ -213,13 +106,13 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
}
|
||||
|
||||
n := mustParseFloat(string(charData), 64)
|
||||
return &plistValue{Real, sizedFloat{n, 64}}
|
||||
return &cfReal{wide: true, value: n}
|
||||
case "true", "false":
|
||||
p.ntags++
|
||||
p.xmlDecoder.Skip()
|
||||
|
||||
b := element.Name.Local == "true"
|
||||
return &plistValue{Boolean, b}
|
||||
return cfBoolean(b)
|
||||
case "date":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
@@ -232,7 +125,7 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Date, t}
|
||||
return cfDate(t)
|
||||
case "data":
|
||||
p.ntags++
|
||||
err := p.xmlDecoder.DecodeElement(&charData, &element)
|
||||
@@ -249,11 +142,12 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &plistValue{Data, bytes[:l]}
|
||||
return cfData(bytes[:l])
|
||||
case "dict":
|
||||
p.ntags++
|
||||
var key *string
|
||||
var subvalues map[string]*plistValue = make(map[string]*plistValue)
|
||||
keys := make([]string, 0, 32)
|
||||
values := make([]cfValue, 0, 32)
|
||||
for {
|
||||
token, err := p.xmlDecoder.Token()
|
||||
if err != nil {
|
||||
@@ -276,15 +170,23 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
if key == nil {
|
||||
panic(errors.New("missing key in dictionary"))
|
||||
}
|
||||
subvalues[*key] = p.parseXMLElement(el)
|
||||
keys = append(keys, *key)
|
||||
values = append(values, p.parseXMLElement(el))
|
||||
key = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return &plistValue{Dictionary, &dictionary{m: subvalues}}
|
||||
|
||||
if len(keys) == 1 && keys[0] == "CF$UID" && len(values) == 1 {
|
||||
if integer, ok := values[0].(*cfNumber); ok {
|
||||
return cfUID(integer.value)
|
||||
}
|
||||
}
|
||||
|
||||
return &cfDictionary{keys: keys, values: values}
|
||||
case "array":
|
||||
p.ntags++
|
||||
var subvalues []*plistValue = make([]*plistValue, 0, 10)
|
||||
values := make([]cfValue, 0, 10)
|
||||
for {
|
||||
token, err := p.xmlDecoder.Token()
|
||||
if err != nil {
|
||||
@@ -296,10 +198,10 @@ func (p *xmlPlistParser) parseXMLElement(element xml.StartElement) *plistValue {
|
||||
}
|
||||
|
||||
if el, ok := token.(xml.StartElement); ok {
|
||||
subvalues = append(subvalues, p.parseXMLElement(el))
|
||||
values = append(values, p.parseXMLElement(el))
|
||||
}
|
||||
}
|
||||
return &plistValue{Array, subvalues}
|
||||
return &cfArray{values}
|
||||
}
|
||||
err := fmt.Errorf("encountered unknown element %s", element.Name.Local)
|
||||
if p.ntags == 0 {
|
||||
20
vendor/github.com/DHowett/go-plist/zerocopy.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// +build !appengine
|
||||
|
||||
package plist
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func zeroCopy8BitString(buf []byte, off int, len int) string {
|
||||
if len == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var s string
|
||||
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
hdr.Data = uintptr(unsafe.Pointer(&buf[off]))
|
||||
hdr.Len = len
|
||||
return s
|
||||
}
|
||||
7
vendor/github.com/DHowett/go-plist/zerocopy_appengine.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build appengine
|
||||
|
||||
package plist
|
||||
|
||||
func zeroCopy8BitString(buf []byte, off int, len int) string {
|
||||
return string(buf[off : off+len])
|
||||
}
|
||||
4
vendor/github.com/alecthomas/kingpin/.travis.yml
generated
vendored
@@ -1,4 +0,0 @@
|
||||
sudo: false
|
||||
language: go
|
||||
install: go get -t -v ./...
|
||||
go: 1.2
|
||||
152
vendor/github.com/alecthomas/kingpin/README.md
generated
vendored
@@ -1,4 +1,7 @@
|
||||
# Kingpin - A Go (golang) command line and flag parser [](https://travis-ci.org/alecthomas/kingpin)
|
||||
# Kingpin - A Go (golang) command line and flag parser
|
||||
[](http://godoc.org/github.com/alecthomas/kingpin) [](https://travis-ci.org/alecthomas/kingpin) [](https://gitter.im/alecthomas/Lobby)
|
||||
|
||||
|
||||
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
@@ -19,9 +22,12 @@
|
||||
- [Displaying errors and usage information](#displaying-errors-and-usage-information)
|
||||
- [Sub-commands](#sub-commands)
|
||||
- [Custom Parsers](#custom-parsers)
|
||||
- [Repeatable flags](#repeatable-flags)
|
||||
- [Boolean Values](#boolean-values)
|
||||
- [Default Values](#default-values)
|
||||
- [Place-holders in Help](#place-holders-in-help)
|
||||
- [Consuming all remaining arguments](#consuming-all-remaining-arguments)
|
||||
- [Bash/ZSH Shell Completion](#bashzsh-shell-completion)
|
||||
- [Supporting -h for help](#supporting--h-for-help)
|
||||
- [Custom help](#custom-help)
|
||||
|
||||
@@ -51,7 +57,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
More [examples](https://github.com/alecthomas/kingpin/tree/master/examples) are available.
|
||||
More [examples](https://github.com/alecthomas/kingpin/tree/master/_examples) are available.
|
||||
|
||||
Second to parsing, providing the user with useful help is probably the most
|
||||
important thing a command-line parser does. Kingpin tries to provide detailed
|
||||
@@ -71,7 +77,7 @@ contextual help if `--help` is encountered at any point in the command line
|
||||
- POSIX-style short flag combining (`-a -b` -> `-ab`).
|
||||
- Short-flag+parameter combining (`-a parm` -> `-aparm`).
|
||||
- Read command-line from files (`@<file>`).
|
||||
- Automatically generate man pages (`--man-page`).
|
||||
- Automatically generate man pages (`--help-man`).
|
||||
|
||||
## User-visible changes between v1 and v2
|
||||
|
||||
@@ -135,7 +141,7 @@ $ go get gopkg.in/alecthomas/kingpin.v1
|
||||
- *2015-09-19* -- Stable v2.1.0 release.
|
||||
- Added `command.Default()` to specify a default command to use if no other
|
||||
command matches. This allows for convenient user shortcuts.
|
||||
- Exposed `HelpFlag` and `VersionFlag` for further cusomisation.
|
||||
- Exposed `HelpFlag` and `VersionFlag` for further customisation.
|
||||
- `Action()` and `PreAction()` added and both now support an arbitrary
|
||||
number of callbacks.
|
||||
- `kingpin.SeparateOptionalFlagsUsageTemplate`.
|
||||
@@ -216,7 +222,7 @@ Args:
|
||||
<ip> IP address to ping.
|
||||
[<count>] Number of packets to send
|
||||
$ ping 1.2.3.4 5
|
||||
Would ping: 1.2.3.4 with timeout 5s and count 0
|
||||
Would ping: 1.2.3.4 with timeout 5s and count 5
|
||||
```
|
||||
|
||||
From the following source:
|
||||
@@ -240,7 +246,7 @@ var (
|
||||
func main() {
|
||||
kingpin.Version("0.0.1")
|
||||
kingpin.Parse()
|
||||
fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count)
|
||||
fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -381,7 +387,7 @@ func main() {
|
||||
|
||||
Kingpin supports both flag and positional argument parsers for converting to
|
||||
Go types. For example, some included parsers are `Int()`, `Float()`,
|
||||
`Duration()` and `ExistingFile()`.
|
||||
`Duration()` and `ExistingFile()` (see [parsers.go](./parsers.go) for a complete list of included parsers).
|
||||
|
||||
Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value)
|
||||
interface, so any existing implementations will work.
|
||||
@@ -409,7 +415,7 @@ As a convenience, I would recommend something like this:
|
||||
|
||||
```go
|
||||
func HTTPHeader(s Settings) (target *http.Header) {
|
||||
target = new(http.Header)
|
||||
target = &http.Header{}
|
||||
s.SetValue((*HTTPHeaderValue)(target))
|
||||
return
|
||||
}
|
||||
@@ -421,12 +427,26 @@ You would use it like so:
|
||||
headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H'))
|
||||
```
|
||||
|
||||
### Repeatable flags
|
||||
|
||||
Depending on the `Value` they hold, some flags may be repeated. The
|
||||
`IsCumulative() bool` function on `Value` tells if it's safe to call `Set()`
|
||||
multiple times or if an error should be raised if several values are passed.
|
||||
|
||||
The built-in `Value`s returning slices and maps, as well as `Counter` are
|
||||
examples of `Value`s that make a flag repeatable.
|
||||
|
||||
### Boolean values
|
||||
|
||||
Boolean values are uniquely managed by Kingpin. Each boolean flag will have a negative complement:
|
||||
`--<name>` and `--no-<name>`.
|
||||
|
||||
### Default Values
|
||||
|
||||
The default value is the zero value for a type. This can be overridden with
|
||||
the `Default(value)` function on flags and arguments. This function accepts a
|
||||
string, which is parsed by the value itself, so it *must* be compliant with
|
||||
the format expected.
|
||||
the `Default(value...)` function on flags and arguments. This function accepts
|
||||
one or several strings, which are parsed by the value itself, so they *must*
|
||||
be compliant with the format expected.
|
||||
|
||||
### Place-holders in Help
|
||||
|
||||
@@ -441,7 +461,7 @@ Here are some examples of flags with various permutations:
|
||||
|
||||
--name=NAME // Flag(...).String()
|
||||
--name="Harry" // Flag(...).Default("Harry").String()
|
||||
--name=FULL-NAME // flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
|
||||
--name=FULL-NAME // Flag(...).PlaceHolder("FULL-NAME").Default("Harry").String()
|
||||
|
||||
### Consuming all remaining arguments
|
||||
|
||||
@@ -451,13 +471,13 @@ IP addresses as positional arguments:
|
||||
|
||||
./cmd ping 10.1.1.1 192.168.1.1
|
||||
|
||||
Kingpin supports this by having `Value` provide a `IsCumulative() bool`
|
||||
function. If this function exists and returns true, the value parser will be
|
||||
called repeatedly for every remaining argument.
|
||||
Such arguments are similar to [repeatable flags](#repeatable-flags), but for
|
||||
arguments. Therefore they use the same `IsCumulative() bool` function on the
|
||||
underlying `Value`, so the built-in `Value`s for which the `Set()` function
|
||||
can be called several times will consume multiple arguments.
|
||||
|
||||
Examples of this are the `Strings()` and `StringMap()` values.
|
||||
|
||||
To implement the above example we might do something like this:
|
||||
To implement the above example with a custom `Value`, we might do something
|
||||
like this:
|
||||
|
||||
```go
|
||||
type ipList []net.IP
|
||||
@@ -492,9 +512,103 @@ And use it like so:
|
||||
ips := IPList(kingpin.Arg("ips", "IP addresses to ping."))
|
||||
```
|
||||
|
||||
### Bash/ZSH Shell Completion
|
||||
|
||||
By default, all flags and commands/subcommands generate completions
|
||||
internally.
|
||||
|
||||
Out of the box, CLI tools using kingpin should be able to take advantage
|
||||
of completion hinting for flags and commands. By specifying
|
||||
`--completion-bash` as the first argument, your CLI tool will show
|
||||
possible subcommands. By ending your argv with `--`, hints for flags
|
||||
will be shown.
|
||||
|
||||
To allow your end users to take advantage you must package a
|
||||
`/etc/bash_completion.d` script with your distribution (or the equivalent
|
||||
for your target platform/shell). An alternative is to instruct your end
|
||||
user to source a script from their `bash_profile` (or equivalent).
|
||||
|
||||
Fortunately Kingpin makes it easy to generate or source a script for use
|
||||
with end users shells. `./yourtool --completion-script-bash` and
|
||||
`./yourtool --completion-script-zsh` will generate these scripts for you.
|
||||
|
||||
**Installation by Package**
|
||||
|
||||
For the best user experience, you should bundle your pre-created
|
||||
completion script with your CLI tool and install it inside
|
||||
`/etc/bash_completion.d` (or equivalent). A good suggestion is to add
|
||||
this as an automated step to your build pipeline, in the implementation
|
||||
is improved for bug fixed.
|
||||
|
||||
**Installation by `bash_profile`**
|
||||
|
||||
Alternatively, instruct your users to add an additional statement to
|
||||
their `bash_profile` (or equivalent):
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-bash)"
|
||||
```
|
||||
|
||||
Or for ZSH
|
||||
|
||||
```
|
||||
eval "$(your-cli-tool --completion-script-zsh)"
|
||||
```
|
||||
|
||||
#### Additional API
|
||||
To provide more flexibility, a completion option API has been
|
||||
exposed for flags to allow user defined completion options, to extend
|
||||
completions further than just EnumVar/Enum.
|
||||
|
||||
|
||||
**Provide Static Options**
|
||||
|
||||
When using an `Enum` or `EnumVar`, users are limited to only the options
|
||||
given. Maybe we wish to hint possible options to the user, but also
|
||||
allow them to provide their own custom option. `HintOptions` gives
|
||||
this functionality to flags.
|
||||
|
||||
```
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("port", "Provide a port to connect to").
|
||||
Required().
|
||||
HintOptions("80", "443", "8080").
|
||||
IntVar(&c.port)
|
||||
```
|
||||
|
||||
**Provide Dynamic Options**
|
||||
Consider the case that you needed to read a local database or a file to
|
||||
provide suggestions. You can dynamically generate the options
|
||||
|
||||
```
|
||||
func listHosts() []string {
|
||||
// Provide a dynamic list of hosts from a hosts file or otherwise
|
||||
// for bash completion. In this example we simply return static slice.
|
||||
|
||||
// You could use this functionality to reach into a hosts file to provide
|
||||
// completion for a list of known hosts.
|
||||
return []string{"sshhost.example", "webhost.example", "ftphost.example"}
|
||||
}
|
||||
|
||||
app := kingpin.New("completion", "My application with bash completion.")
|
||||
app.Flag("flag-1", "").HintAction(listHosts).String()
|
||||
```
|
||||
|
||||
**EnumVar/Enum**
|
||||
When using `Enum` or `EnumVar`, any provided options will be automatically
|
||||
used for bash autocompletion. However, if you wish to provide a subset or
|
||||
different options, you can use `HintOptions` or `HintAction` which will override
|
||||
the default completion options for `Enum`/`EnumVar`.
|
||||
|
||||
|
||||
**Examples**
|
||||
You can see an in depth example of the completion API within
|
||||
`examples/completion/main.go`
|
||||
|
||||
|
||||
### Supporting -h for help
|
||||
|
||||
`kingpin.CommandLine.HelpFlag.Short('-h')`
|
||||
`kingpin.CommandLine.HelpFlag.Short('h')`
|
||||
|
||||
### Custom help
|
||||
|
||||
|
||||
279
vendor/github.com/alecthomas/kingpin/app.go
generated
vendored
@@ -13,7 +13,7 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z_]+`)
|
||||
envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
|
||||
)
|
||||
|
||||
type ApplicationValidator func(*Application) error
|
||||
@@ -21,10 +21,7 @@ type ApplicationValidator func(*Application) error
|
||||
// An Application contains the definitions of flags, arguments and commands
|
||||
// for an application.
|
||||
type Application struct {
|
||||
*flagGroup
|
||||
*argGroup
|
||||
*cmdGroup
|
||||
actionMixin
|
||||
cmdMixin
|
||||
initialized bool
|
||||
|
||||
Name string
|
||||
@@ -32,12 +29,14 @@ type Application struct {
|
||||
|
||||
author string
|
||||
version string
|
||||
writer io.Writer // Destination for usage and errors.
|
||||
errorWriter io.Writer // Destination for errors.
|
||||
usageWriter io.Writer // Destination for usage
|
||||
usageTemplate string
|
||||
validator ApplicationValidator
|
||||
terminate func(status int) // See Terminate()
|
||||
noInterspersed bool // can flags be interspersed with args (or must they come first)
|
||||
defaultEnvars bool
|
||||
completion bool
|
||||
|
||||
// Help flag. Exposed for user customisation.
|
||||
HelpFlag *FlagClause
|
||||
@@ -50,19 +49,24 @@ type Application struct {
|
||||
// New creates a new Kingpin application instance.
|
||||
func New(name, help string) *Application {
|
||||
a := &Application{
|
||||
flagGroup: newFlagGroup(),
|
||||
argGroup: newArgGroup(),
|
||||
Name: name,
|
||||
Help: help,
|
||||
writer: os.Stderr,
|
||||
errorWriter: os.Stderr, // Left for backwards compatibility purposes.
|
||||
usageWriter: os.Stderr,
|
||||
usageTemplate: DefaultUsageTemplate,
|
||||
terminate: os.Exit,
|
||||
}
|
||||
a.flagGroup = newFlagGroup()
|
||||
a.argGroup = newArgGroup()
|
||||
a.cmdGroup = newCmdGroup(a)
|
||||
a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).")
|
||||
a.HelpFlag.Bool()
|
||||
a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool()
|
||||
a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool()
|
||||
a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion)
|
||||
a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool()
|
||||
a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -84,6 +88,24 @@ func (a *Application) generateManPage(c *ParseContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) generateZSHCompletionScript(c *ParseContext) error {
|
||||
a.Writer(os.Stdout)
|
||||
if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
a.terminate(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultEnvars configures all flags (that do not already have an associated
|
||||
// envar) to use a default environment variable in the form "<app>_<flag>".
|
||||
//
|
||||
@@ -104,9 +126,23 @@ func (a *Application) Terminate(terminate func(int)) *Application {
|
||||
return a
|
||||
}
|
||||
|
||||
// Specify the writer to use for usage and errors. Defaults to os.Stderr.
|
||||
// Writer specifies the writer to use for usage and errors. Defaults to os.Stderr.
|
||||
// DEPRECATED: See ErrorWriter and UsageWriter.
|
||||
func (a *Application) Writer(w io.Writer) *Application {
|
||||
a.writer = w
|
||||
a.errorWriter = w
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// ErrorWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) ErrorWriter(w io.Writer) *Application {
|
||||
a.errorWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
// UsageWriter sets the io.Writer to use for errors.
|
||||
func (a *Application) UsageWriter(w io.Writer) *Application {
|
||||
a.usageWriter = w
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -145,17 +181,48 @@ func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseCon
|
||||
// This will populate all flag and argument values, call all callbacks, and so
|
||||
// on.
|
||||
func (a *Application) Parse(args []string) (command string, err error) {
|
||||
context, err := a.ParseContext(args)
|
||||
if err != nil {
|
||||
|
||||
context, parseErr := a.ParseContext(args)
|
||||
selected := []string{}
|
||||
var setValuesErr error
|
||||
|
||||
if context == nil {
|
||||
// Since we do not throw error immediately, there could be a case
|
||||
// where a context returns nil. Protect against that.
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
if err = a.setDefaults(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
a.maybeHelp(context)
|
||||
if !context.EOL() {
|
||||
return "", fmt.Errorf("unexpected argument '%s'", context.Peek())
|
||||
|
||||
selected, setValuesErr = a.setValues(context)
|
||||
|
||||
if err = a.applyPreActions(context, !a.completion); err != nil {
|
||||
return "", err
|
||||
}
|
||||
command, err = a.execute(context)
|
||||
if err == ErrCommandNotSpecified {
|
||||
a.writeUsage(context, nil)
|
||||
|
||||
if a.completion {
|
||||
a.generateBashCompletion(context)
|
||||
a.terminate(0)
|
||||
} else {
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
a.maybeHelp(context)
|
||||
if !context.EOL() {
|
||||
return "", fmt.Errorf("unexpected argument '%s'", context.Peek())
|
||||
}
|
||||
|
||||
if setValuesErr != nil {
|
||||
return "", setValuesErr
|
||||
}
|
||||
|
||||
command, err = a.execute(context, selected)
|
||||
if err == ErrCommandNotSpecified {
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
return command, err
|
||||
}
|
||||
@@ -167,45 +234,28 @@ func (a *Application) writeUsage(context *ParseContext, err error) {
|
||||
if err := a.UsageForContext(context); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.terminate(1)
|
||||
if err != nil {
|
||||
a.terminate(1)
|
||||
} else {
|
||||
a.terminate(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) maybeHelp(context *ParseContext) {
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag {
|
||||
// Re-parse the command-line ignoring defaults, so that help works correctly.
|
||||
context, _ = a.parseContext(true, context.rawArgs)
|
||||
a.writeUsage(context, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findCommandFromArgs finds a command (if any) from the given command line arguments.
|
||||
func (a *Application) findCommandFromArgs(args []string) (command string, err error) {
|
||||
if err := a.init(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
context := tokenize(args, false)
|
||||
if _, err := a.parse(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return a.findCommandFromContext(context), nil
|
||||
}
|
||||
|
||||
// findCommandFromContext finds a command (if any) from a parsed context.
|
||||
func (a *Application) findCommandFromContext(context *ParseContext) string {
|
||||
commands := []string{}
|
||||
for _, element := range context.Elements {
|
||||
if c, ok := element.Clause.(*CmdClause); ok {
|
||||
commands = append(commands, c.name)
|
||||
}
|
||||
}
|
||||
return strings.Join(commands, " ")
|
||||
}
|
||||
|
||||
// Version adds a --version flag for displaying the application version.
|
||||
func (a *Application) Version(version string) *Application {
|
||||
a.version = version
|
||||
a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error {
|
||||
fmt.Fprintln(a.writer, version)
|
||||
fmt.Fprintln(a.usageWriter, version)
|
||||
a.terminate(0)
|
||||
return nil
|
||||
})
|
||||
@@ -213,6 +263,7 @@ func (a *Application) Version(version string) *Application {
|
||||
return a
|
||||
}
|
||||
|
||||
// Author sets the author output by some help templates.
|
||||
func (a *Application) Author(author string) *Application {
|
||||
a.author = author
|
||||
return a
|
||||
@@ -325,22 +376,8 @@ func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) execute(context *ParseContext) (string, error) {
|
||||
func (a *Application) execute(context *ParseContext, selected []string) (string, error) {
|
||||
var err error
|
||||
selected := []string{}
|
||||
|
||||
if err = a.setDefaults(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
selected, err = a.setValues(context)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.applyPreActions(context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.validateRequired(context); err != nil {
|
||||
return "", err
|
||||
@@ -365,6 +402,9 @@ func (a *Application) setDefaults(context *ParseContext) error {
|
||||
flagElements := map[string]*ParseElement{}
|
||||
for _, element := range context.Elements {
|
||||
if flag, ok := element.Clause.(*FlagClause); ok {
|
||||
if flag.name == "help" {
|
||||
return nil
|
||||
}
|
||||
flagElements[flag.name] = element
|
||||
}
|
||||
}
|
||||
@@ -379,22 +419,16 @@ func (a *Application) setDefaults(context *ParseContext) error {
|
||||
// Check required flags and set defaults.
|
||||
for _, flag := range context.flags.long {
|
||||
if flagElements[flag.name] == nil {
|
||||
// Set defaults, if any.
|
||||
if flag.defaultValue != "" {
|
||||
if err := flag.value.Set(flag.defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := flag.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
// Set defaults, if any.
|
||||
if arg.defaultValue != "" {
|
||||
if err := arg.value.Set(arg.defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := arg.setDefault(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +463,7 @@ func (a *Application) validateRequired(context *ParseContext) error {
|
||||
|
||||
for _, arg := range context.arguments.args {
|
||||
if argElements[arg.name] == nil {
|
||||
if arg.required {
|
||||
if arg.needsValue() {
|
||||
return fmt.Errorf("required argument '%s' not provided", arg.name)
|
||||
}
|
||||
}
|
||||
@@ -439,13 +473,22 @@ func (a *Application) validateRequired(context *ParseContext) error {
|
||||
|
||||
func (a *Application) setValues(context *ParseContext) (selected []string, err error) {
|
||||
// Set all arg and flag values.
|
||||
var lastCmd *CmdClause
|
||||
var (
|
||||
lastCmd *CmdClause
|
||||
flagSet = map[string]struct{}{}
|
||||
)
|
||||
for _, element := range context.Elements {
|
||||
switch clause := element.Clause.(type) {
|
||||
case *FlagClause:
|
||||
if _, ok := flagSet[clause.name]; ok {
|
||||
if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() {
|
||||
return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name)
|
||||
}
|
||||
}
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
return
|
||||
}
|
||||
flagSet[clause.name] = struct{}{}
|
||||
|
||||
case *ArgClause:
|
||||
if err = clause.value.Set(*element.Value); err != nil {
|
||||
@@ -486,18 +529,21 @@ func (a *Application) applyValidators(context *ParseContext) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Application) applyPreActions(context *ParseContext) error {
|
||||
func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error {
|
||||
if err := a.actionMixin.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
// Dispatch to actions.
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyPreActions(context); err != nil {
|
||||
return err
|
||||
if dispatch {
|
||||
for _, element := range context.Elements {
|
||||
if applier, ok := element.Clause.(actionApplier); ok {
|
||||
if err := applier.applyPreActions(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -518,7 +564,7 @@ func (a *Application) applyActions(context *ParseContext) error {
|
||||
|
||||
// Errorf prints an error message to w in the format "<appname>: error: <message>".
|
||||
func (a *Application) Errorf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(a.writer, a.Name+": error: "+format+"\n", args...)
|
||||
fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// Fatalf writes a formatted error to w then terminates with exit status 1.
|
||||
@@ -531,6 +577,8 @@ func (a *Application) Fatalf(format string, args ...interface{}) {
|
||||
// exits with a non-zero status.
|
||||
func (a *Application) FatalUsage(format string, args ...interface{}) {
|
||||
a.Errorf(format, args...)
|
||||
// Force usage to go to error output.
|
||||
a.usageWriter = a.errorWriter
|
||||
a.Usage([]string{})
|
||||
a.terminate(1)
|
||||
}
|
||||
@@ -558,6 +606,83 @@ func (a *Application) FatalIfError(err error, format string, args ...interface{}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) completionOptions(context *ParseContext) []string {
|
||||
args := context.rawArgs
|
||||
|
||||
var (
|
||||
currArg string
|
||||
prevArg string
|
||||
target cmdMixin
|
||||
)
|
||||
|
||||
numArgs := len(args)
|
||||
if numArgs > 1 {
|
||||
args = args[1:]
|
||||
currArg = args[len(args)-1]
|
||||
}
|
||||
if numArgs > 2 {
|
||||
prevArg = args[len(args)-2]
|
||||
}
|
||||
|
||||
target = a.cmdMixin
|
||||
if context.SelectedCommand != nil {
|
||||
// A subcommand was in use. We will use it as the target
|
||||
target = context.SelectedCommand.cmdMixin
|
||||
}
|
||||
|
||||
if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") {
|
||||
// Perform completion for A flag. The last/current argument started with "-"
|
||||
var (
|
||||
flagName string // The name of a flag if given (could be half complete)
|
||||
flagValue string // The value assigned to a flag (if given) (could be half complete)
|
||||
)
|
||||
|
||||
if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag value
|
||||
// Wont Match: ./myApp --flag --
|
||||
flagName = prevArg[2:] // Strip the "--"
|
||||
flagValue = currArg
|
||||
} else if strings.HasPrefix(currArg, "--") {
|
||||
// Matches: ./myApp --flag --
|
||||
// Matches: ./myApp --flag somevalue --
|
||||
// Matches: ./myApp --
|
||||
flagName = currArg[2:] // Strip the "--"
|
||||
}
|
||||
|
||||
options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue)
|
||||
if valueMatched {
|
||||
// Value Matched. Show cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
// Add top level flags if we're not at the top level and no match was found.
|
||||
if context.SelectedCommand != nil && !flagMatched {
|
||||
topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue)
|
||||
if topValueMatched {
|
||||
// Value Matched. Back to cmdCompletions
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
if topFlagMatched {
|
||||
// Top level had a flag which matched the input. Return it's options.
|
||||
options = topOptions
|
||||
} else {
|
||||
// Add top level flags
|
||||
options = append(options, topOptions...)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// Perform completion for sub commands and arguments.
|
||||
return target.CmdCompletion(context)
|
||||
}
|
||||
|
||||
func (a *Application) generateBashCompletion(context *ParseContext) {
|
||||
options := a.completionOptions(context)
|
||||
fmt.Printf("%s", strings.Join(options, "\n"))
|
||||
}
|
||||
|
||||
func envarTransform(name string) string {
|
||||
return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_"))
|
||||
}
|
||||
|
||||
97
vendor/github.com/alecthomas/kingpin/args.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
package kingpin
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type argGroup struct {
|
||||
args []*ArgClause
|
||||
@@ -14,6 +16,19 @@ func (a *argGroup) have() bool {
|
||||
return len(a.args) > 0
|
||||
}
|
||||
|
||||
// GetArg gets an argument definition.
|
||||
//
|
||||
// This allows existing arguments to be modified after definition but before parsing. Useful for
|
||||
// modular applications.
|
||||
func (a *argGroup) GetArg(name string) *ArgClause {
|
||||
for _, arg := range a.args {
|
||||
if arg.name == name {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *argGroup) Arg(name, help string) *ArgClause {
|
||||
arg := newArg(name, help)
|
||||
a.args = append(a.args, arg)
|
||||
@@ -51,10 +66,12 @@ func (a *argGroup) init() error {
|
||||
type ArgClause struct {
|
||||
actionMixin
|
||||
parserMixin
|
||||
name string
|
||||
help string
|
||||
defaultValue string
|
||||
required bool
|
||||
completionsMixin
|
||||
envarMixin
|
||||
name string
|
||||
help string
|
||||
defaultValues []string
|
||||
required bool
|
||||
}
|
||||
|
||||
func newArg(name, help string) *ArgClause {
|
||||
@@ -65,6 +82,37 @@ func newArg(name, help string) *ArgClause {
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) setDefault() error {
|
||||
if a.HasEnvarValue() {
|
||||
if v, ok := a.value.(remainderArg); !ok || !v.IsCumulative() {
|
||||
// Use the value as-is
|
||||
return a.value.Set(a.GetEnvarValue())
|
||||
}
|
||||
for _, value := range a.GetSplitEnvarValue() {
|
||||
if err := a.value.Set(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(a.defaultValues) > 0 {
|
||||
for _, defaultValue := range a.defaultValues {
|
||||
if err := a.value.Set(defaultValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArgClause) needsValue() bool {
|
||||
haveDefault := len(a.defaultValues) > 0
|
||||
return a.required && !(haveDefault || a.HasEnvarValue())
|
||||
}
|
||||
|
||||
func (a *ArgClause) consumesRemainder() bool {
|
||||
if r, ok := a.value.(remainderArg); ok {
|
||||
return r.IsCumulative()
|
||||
@@ -78,9 +126,26 @@ func (a *ArgClause) Required() *ArgClause {
|
||||
return a
|
||||
}
|
||||
|
||||
// Default value for this argument. It *must* be parseable by the value of the argument.
|
||||
func (a *ArgClause) Default(value string) *ArgClause {
|
||||
a.defaultValue = value
|
||||
// Default values for this argument. They *must* be parseable by the value of the argument.
|
||||
func (a *ArgClause) Default(values ...string) *ArgClause {
|
||||
a.defaultValues = values
|
||||
return a
|
||||
}
|
||||
|
||||
// Envar overrides the default value(s) for a flag from an environment variable,
|
||||
// if it is set. Several default values can be provided by using new lines to
|
||||
// separate them.
|
||||
func (a *ArgClause) Envar(name string) *ArgClause {
|
||||
a.envar = name
|
||||
a.noEnvar = false
|
||||
return a
|
||||
}
|
||||
|
||||
// NoEnvar forces environment variable defaults to be disabled for this flag.
|
||||
// Most useful in conjunction with app.DefaultEnvars().
|
||||
func (a *ArgClause) NoEnvar() *ArgClause {
|
||||
a.envar = ""
|
||||
a.noEnvar = true
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -94,8 +159,22 @@ func (a *ArgClause) PreAction(action Action) *ArgClause {
|
||||
return a
|
||||
}
|
||||
|
||||
// HintAction registers a HintAction (function) for the arg to provide completions
|
||||
func (a *ArgClause) HintAction(action HintAction) *ArgClause {
|
||||
a.addHintAction(action)
|
||||
return a
|
||||
}
|
||||
|
||||
// HintOptions registers any number of options for the flag to provide completions
|
||||
func (a *ArgClause) HintOptions(options ...string) *ArgClause {
|
||||
a.addHintAction(func() []string {
|
||||
return options
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArgClause) init() error {
|
||||
if a.required && a.defaultValue != "" {
|
||||
if a.required && len(a.defaultValues) > 0 {
|
||||
return fmt.Errorf("required argument '%s' with unusable default value", a.name)
|
||||
}
|
||||
if a.value == nil {
|
||||
|
||||
145
vendor/github.com/alecthomas/kingpin/cmd.go
generated
vendored
@@ -5,6 +5,92 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cmdMixin struct {
|
||||
*flagGroup
|
||||
*argGroup
|
||||
*cmdGroup
|
||||
actionMixin
|
||||
}
|
||||
|
||||
// CmdCompletion returns completion options for arguments, if that's where
|
||||
// parsing left off, or commands if there aren't any unsatisfied args.
|
||||
func (c *cmdMixin) CmdCompletion(context *ParseContext) []string {
|
||||
var options []string
|
||||
|
||||
// Count args already satisfied - we won't complete those, and add any
|
||||
// default commands' alternatives, since they weren't listed explicitly
|
||||
// and the user may want to explicitly list something else.
|
||||
argsSatisfied := 0
|
||||
for _, el := range context.Elements {
|
||||
switch clause := el.Clause.(type) {
|
||||
case *ArgClause:
|
||||
if el.Value != nil && *el.Value != "" {
|
||||
argsSatisfied++
|
||||
}
|
||||
case *CmdClause:
|
||||
options = append(options, clause.completionAlts...)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if argsSatisfied < len(c.argGroup.args) {
|
||||
// Since not all args have been satisfied, show options for the current one
|
||||
options = append(options, c.argGroup.args[argsSatisfied].resolveCompletions()...)
|
||||
} else {
|
||||
// If all args are satisfied, then go back to completing commands
|
||||
for _, cmd := range c.cmdGroup.commandOrder {
|
||||
if !cmd.hidden {
|
||||
options = append(options, cmd.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (choices []string, flagMatch bool, optionMatch bool) {
|
||||
// Check if flagName matches a known flag.
|
||||
// If it does, show the options for the flag
|
||||
// Otherwise, show all flags
|
||||
|
||||
options := []string{}
|
||||
|
||||
for _, flag := range c.flagGroup.flagOrder {
|
||||
// Loop through each flag and determine if a match exists
|
||||
if flag.name == flagName {
|
||||
// User typed entire flag. Need to look for flag options.
|
||||
options = flag.resolveCompletions()
|
||||
if len(options) == 0 {
|
||||
// No Options to Choose From, Assume Match.
|
||||
return options, true, true
|
||||
}
|
||||
|
||||
// Loop options to find if the user specified value matches
|
||||
isPrefix := false
|
||||
matched := false
|
||||
|
||||
for _, opt := range options {
|
||||
if flagValue == opt {
|
||||
matched = true
|
||||
} else if strings.HasPrefix(opt, flagValue) {
|
||||
isPrefix = true
|
||||
}
|
||||
}
|
||||
|
||||
// Matched Flag Directly
|
||||
// Flag Value Not Prefixed, and Matched Directly
|
||||
return options, true, !isPrefix && matched
|
||||
}
|
||||
|
||||
if !flag.hidden {
|
||||
options = append(options, "--"+flag.name)
|
||||
}
|
||||
}
|
||||
// No Flag directly matched.
|
||||
return options, false, false
|
||||
|
||||
}
|
||||
|
||||
type cmdGroup struct {
|
||||
app *Application
|
||||
parent *CmdClause
|
||||
@@ -21,6 +107,22 @@ func (c *cmdGroup) defaultSubcommand() *CmdClause {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cmdGroup) cmdNames() []string {
|
||||
names := make([]string, 0, len(c.commandOrder))
|
||||
for _, cmd := range c.commandOrder {
|
||||
names = append(names, cmd.name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// GetArg gets a command definition.
|
||||
//
|
||||
// This allows existing commands to be modified after definition but before parsing. Useful for
|
||||
// modular applications.
|
||||
func (c *cmdGroup) GetCommand(name string) *CmdClause {
|
||||
return c.commands[name]
|
||||
}
|
||||
|
||||
func newCmdGroup(app *Application) *cmdGroup {
|
||||
return &cmdGroup{
|
||||
app: app,
|
||||
@@ -59,6 +161,12 @@ func (c *cmdGroup) init() error {
|
||||
return fmt.Errorf("duplicate command %q", cmd.name)
|
||||
}
|
||||
seen[cmd.name] = true
|
||||
for _, alias := range cmd.aliases {
|
||||
if seen[alias] {
|
||||
return fmt.Errorf("alias duplicates existing command %q", alias)
|
||||
}
|
||||
c.commands[alias] = cmd
|
||||
}
|
||||
if err := cmd.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,27 +186,32 @@ type CmdClauseValidator func(*CmdClause) error
|
||||
// A CmdClause is a single top-level command. It encapsulates a set of flags
|
||||
// and either subcommands or positional arguments.
|
||||
type CmdClause struct {
|
||||
actionMixin
|
||||
*flagGroup
|
||||
*argGroup
|
||||
*cmdGroup
|
||||
app *Application
|
||||
name string
|
||||
help string
|
||||
isDefault bool
|
||||
validator CmdClauseValidator
|
||||
hidden bool
|
||||
cmdMixin
|
||||
app *Application
|
||||
name string
|
||||
aliases []string
|
||||
help string
|
||||
isDefault bool
|
||||
validator CmdClauseValidator
|
||||
hidden bool
|
||||
completionAlts []string
|
||||
}
|
||||
|
||||
func newCommand(app *Application, name, help string) *CmdClause {
|
||||
c := &CmdClause{
|
||||
flagGroup: newFlagGroup(),
|
||||
argGroup: newArgGroup(),
|
||||
cmdGroup: newCmdGroup(app),
|
||||
app: app,
|
||||
name: name,
|
||||
help: help,
|
||||
app: app,
|
||||
name: name,
|
||||
help: help,
|
||||
}
|
||||
c.flagGroup = newFlagGroup()
|
||||
c.argGroup = newArgGroup()
|
||||
c.cmdGroup = newCmdGroup(app)
|
||||
return c
|
||||
}
|
||||
|
||||
// Add an Alias for this command.
|
||||
func (c *CmdClause) Alias(name string) *CmdClause {
|
||||
c.aliases = append(c.aliases, name)
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
33
vendor/github.com/alecthomas/kingpin/completions.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package kingpin
|
||||
|
||||
// HintAction is a function type who is expected to return a slice of possible
|
||||
// command line arguments.
|
||||
type HintAction func() []string
|
||||
type completionsMixin struct {
|
||||
hintActions []HintAction
|
||||
builtinHintActions []HintAction
|
||||
}
|
||||
|
||||
func (a *completionsMixin) addHintAction(action HintAction) {
|
||||
a.hintActions = append(a.hintActions, action)
|
||||
}
|
||||
|
||||
// Allow adding of HintActions which are added internally, ie, EnumVar
|
||||
func (a *completionsMixin) addHintActionBuiltin(action HintAction) {
|
||||
a.builtinHintActions = append(a.builtinHintActions, action)
|
||||
}
|
||||
|
||||
func (a *completionsMixin) resolveCompletions() []string {
|
||||
var hints []string
|
||||
|
||||
options := a.builtinHintActions
|
||||
if len(a.hintActions) > 0 {
|
||||
// User specified their own hintActions. Use those instead.
|
||||
options = a.hintActions
|
||||
}
|
||||
|
||||
for _, hintAction := range options {
|
||||
hints = append(hints, hintAction()...)
|
||||
}
|
||||
return hints
|
||||
}
|
||||
2
vendor/github.com/alecthomas/kingpin/doc.go
generated
vendored
@@ -35,7 +35,7 @@
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import "gopkg.in/alecthomas/kingpin.v1"
|
||||
// import "gopkg.in/alecthomas/kingpin.v2"
|
||||
//
|
||||
// var (
|
||||
// debug = kingpin.Flag("debug", "enable debug mode").Default("false").Bool()
|
||||
|
||||