69 Commits
1.0.2 ... 1.0.5

Author SHA1 Message Date
codeskyblue
1c60ccc233 set homebrew-tap 2018-09-25 18:51:02 +08:00
codeskyblue
3175093adf fix go version 2018-09-25 16:15:06 +08:00
codeskyblue
72a9609acb update travis 2018-09-25 16:12:11 +08:00
codeskyblue
b2923eaa4a use cached template when use vfs mode 2018-09-25 15:59:38 +08:00
codeskyblue
c3218a122f update doc to latest 2018-09-25 14:35:41 +08:00
codeskyblue
65bb5c6302 add comment 2018-09-18 13:38:41 +08:00
codeskyblue
0cacad260e support folder delete, prevent ghs.yml see 2018-09-18 13:35:10 +08:00
codeskyblue
40f02fba65 add ca-certificates for openid 2018-09-17 21:44:45 +08:00
codeskyblue
e5bb87aaa1 add missing bootstrap 2018-09-17 21:05:15 +08:00
codeskyblue
80b84ad6b9 remove res 2018-09-17 21:01:50 +08:00
codeskyblue
395537a5d2 fix conflicts 2018-09-17 20:37:56 +08:00
codeskyblue
dc03b85283 fix 2018-09-17 20:35:25 +08:00
codeskyblue
56aec4c702 fix with new dockerfile 2018-09-17 20:34:55 +08:00
codeskyblue
951128c735 fix with new dockerfile 2018-09-17 20:27:10 +08:00
codeskyblue
8e4187fe19 use vfsgen instead gobindata 2018-09-14 21:03:27 +08:00
Jiawen Geng
403a2ea84b Update Docker Usage 2017-12-27 18:54:38 +08:00
gengjiawen
7f4444b8a3 update README 2017-12-27 08:02:40 +08:00
Fuxin Hao
bd7666b3e6 Use multi-stage builds 2017-12-27 08:02:40 +08:00
Fuxin Hao
876b8cffc4 Correct dependencies 2017-12-27 08:02:40 +08:00
gengjiawen
6bfa914a51 fix 2017-12-27 08:02:40 +08:00
gengjiawen
24ef16e80e initial support 2017-12-27 08:02:40 +08:00
shengxiang
cc6b6987f8 update access-log 2017-12-15 10:07:56 +08:00
codeskyblue
1b217ca440 remove 1g limit 2017-09-11 13:40:05 +08:00
codeskyblue
7587b8067a add travis and goreleaser 2017-09-11 12:59:02 +08:00
codeskyblue
2b7fb84a5e update doc 2017-09-11 12:56:04 +08:00
codeskyblue
963f2ddb32 update doc 2017-08-31 23:35:42 +08:00
codeskyblue
3ff2620ba5 return json response when upload 2017-06-14 23:06:24 +08:00
Wei
22c83d8f2d Reword README
Fixed a few English language problem.
2017-06-08 23:47:45 +08:00
codeskyblue
cab407a098 sync to new androidbinary lib 2017-06-01 17:00:23 +08:00
codeskyblue
4d21cd5dd8 fix js error 2017-05-22 23:00:40 +08:00
codeskyblue
26cb73de1c remove useless console.log 2017-05-22 22:41:41 +08:00
codeskyblue
caef8c0fbd make qrcode page large 2017-05-22 19:11:34 +08:00
codeskyblue
ea3d3fdcef wechat is support appstore link now, no need to show wechat redirect page anymore 2017-05-22 17:06:41 +08:00
codeskyblue
4d2c5587d6 remove temp file associated with form after uploaded 2017-05-19 16:04:24 +08:00
codeskyblue
6b3f0f0ae6 quick hot fix androidbinary panic bug 2017-05-17 14:59:32 +08:00
codeskyblue
28e9a2c0ca fix little bug 2017-05-17 10:46:35 +08:00
codeskyblue
e5677bec86 close #18 support show folder size 2017-05-17 10:28:00 +08:00
codeskyblue
a1b16115df update api 2017-05-16 21:41:41 +08:00
codeskyblue
5439149b09 update vendor 2017-05-16 21:34:30 +08:00
codeskyblue
332276f6db disable auto recover 2017-05-11 17:07:05 +08:00
codeskyblue
66c45e1c6e add remove all upload button 2017-05-11 16:50:20 +08:00
codeskyblue
68654f8bd3 add some log 2017-05-11 16:47:17 +08:00
codeskyblue
a95e38802d prevent out of memory 2017-05-11 16:20:37 +08:00
codeskyblue
e063187656 add missing package 2017-05-09 17:17:53 +08:00
codeskyblue
892546343a fix parse apk panic 2017-05-09 17:10:58 +08:00
codeskyblue
dc3b5695d4 remove 1.5 test 2017-05-09 16:42:17 +08:00
codeskyblue
f8ee0c0459 add api -/apk/info/:path-of-apk 2017-05-09 16:38:29 +08:00
codeskyblue
947bbe9efa fix #16 2017-02-14 13:10:31 +08:00
codeskyblue
9af2b5b5f6 fix #8 2016-12-22 14:42:24 +08:00
codeskyblue
7e1387a787 fix display login logic. 2016-12-22 12:05:34 +08:00
codeskyblue
ff1f38c494 add openid support2 2016-12-22 11:46:29 +08:00
codeskyblue
de1d2ba243 Merge branch 'master' of https://github.com/codeskyblue/gohttpserver 2016-12-21 15:47:12 +08:00
codeskyblue
2ab2c553b6 add accessTables support 2016-12-21 15:47:03 +08:00
codeskyblue
6889309af7 format index.tmpl.html code 2016-12-21 15:15:17 +08:00
codeskyblue
61758ec3ce add HEAD support, close #10 2016-09-21 20:20:13 +08:00
codeskyblue
7a1831c6ec fix breadcrumb when path is /somedir/, got different feeling of /somedir 2016-08-26 13:34:08 +08:00
codeskyblue
31cafdfd16 fix make index error 2016-08-24 11:26:10 +08:00
codeskyblue
859574f1e0 fix search path error when root not . 2016-08-12 11:02:54 +08:00
codeskyblue
b4efbd1b6b remove some useless code 2016-08-06 22:25:08 +08:00
codeskyblue
a7fc7054c5 format size in browser 2016-08-05 22:15:57 +08:00
codeskyblue
9dd2d6da30 add relevent link 2016-08-05 17:36:13 +08:00
codeskyblue
ad85705599 use fontawesome copy icon 2016-08-05 17:23:13 +08:00
codeskyblue
b94703f6b9 fix sort 2016-08-05 17:13:29 +08:00
codeskyblue
acad061682 sort by time desc 2016-08-05 16:53:44 +08:00
codeskyblue
d89a841bfb add clipboard support 2016-08-05 13:42:52 +08:00
codeskyblue
b6a2908b68 load js when load finished 2016-08-05 11:22:24 +08:00
codeskyblue
37b71cb7bf add history back button 2016-08-05 11:13:25 +08:00
codeskyblue
644cbbcfd6 close preview now 2016-08-04 20:02:12 +08:00
codeskyblue
f9b2518734 load preview file 2016-08-04 19:59:00 +08:00
257 changed files with 25346 additions and 4710 deletions

2
.ghs.yml Normal file
View File

@@ -0,0 +1,2 @@
upload: true
delete: false

3
.gitignore vendored
View File

@@ -23,8 +23,7 @@ _testmain.go
*.test
*.prof
dist/
gohttpserver
bindata_assetfs.go
assets_vfsdata.go

44
.goreleaser.yml Normal file
View 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'

View File

@@ -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
View 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
View File

@@ -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
View File

@@ -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.

View File

@@ -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
View File

@@ -1,19 +1,18 @@
# gohttpserver
[![Build Status](https://travis-ci.org/codeskyblue/gohttpserver.svg?branch=master)](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
![screen](testdata/filetypes/gohttpserver.gif)
@@ -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).

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -24,3 +24,11 @@ div.dropzone {
.qrcode-title {
font-size: 0.8em;
}
.clearfix::after {
clear: both;
}
#qrcodeCanvas {
padding-right: 20px;
}

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View File

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 698 B

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

275
assets/index.html Normal file
View 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">&times;</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">&times;</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">&times;</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
View 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

File diff suppressed because one or more lines are too long

406
assets/js/index.js Normal file
View 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();
});
});

File diff suppressed because it is too large Load Diff

5
assets/js/showdown-1.6.4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

9
assets_dev.go Normal file
View 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
View 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)
}
}

View File

@@ -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
View File

@@ -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
View 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)
})
}

View File

@@ -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">&times;</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">&times;</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>

View File

@@ -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>

View File

@@ -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();
})

File diff suppressed because one or more lines are too long

View File

@@ -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))
}
}

View File

@@ -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))
}
}

View File

@@ -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
View File

0
testdata/deletable/other.file vendored Normal file
View File

0
testdata/deletable/visual.file vendored Normal file
View File

View File

@@ -1,2 +1,7 @@
---
upload: true
upload: false
users:
- email: "user@example.com"
upload: true
delete: true
token: 123456

View File

View File

@@ -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 ""
}

View File

@@ -1,6 +1,8 @@
# plist - A pure Go property list transcoder
# plist - A pure Go property list transcoder [![coverage report](https://gitlab.howett.net/go/plist/badges/master/coverage.svg)](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

View File

@@ -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
View 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
View 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}
}

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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
}

View File

@@ -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
View 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)
}

View File

@@ -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
View 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
View 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,
}
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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
View 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)}
}

View File

@@ -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
View 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
}

View File

@@ -0,0 +1,7 @@
// +build appengine
package plist
func zeroCopy8BitString(buf []byte, off int, len int) string {
return string(buf[off : off+len])
}

View File

@@ -1,4 +0,0 @@
sudo: false
language: go
install: go get -t -v ./...
go: 1.2

View File

@@ -1,4 +1,7 @@
# Kingpin - A Go (golang) command line and flag parser [![Build Status](https://travis-ci.org/alecthomas/kingpin.png)](https://travis-ci.org/alecthomas/kingpin)
# Kingpin - A Go (golang) command line and flag parser
[![](https://godoc.org/github.com/alecthomas/kingpin?status.svg)](http://godoc.org/github.com/alecthomas/kingpin) [![Build Status](https://travis-ci.org/alecthomas/kingpin.svg?branch=master)](https://travis-ci.org/alecthomas/kingpin) [![Gitter chat](https://badges.gitter.im/alecthomas.png)](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

View File

@@ -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, "_"))
}

View File

@@ -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 {

View File

@@ -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
View 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
}

View File

@@ -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()

Some files were not shown because too many files have changed in this diff Show More