├── .gitignore ├── LICENSE ├── README.md ├── deps ├── coro-channel.lua ├── coro-fs.lua ├── coro-tcp.lua ├── coro-wrapper.lua ├── git.lua ├── hex-bin.lua ├── http-codec.lua ├── json.lua ├── pretty-print │ ├── 16.lua │ ├── 256.lua │ └── init.lua └── require.lua ├── index.html ├── libs ├── auto-headers.lua ├── etag-cache.lua ├── git-fs.lua ├── git-hash-cache.lua ├── git-ref-cache.lua ├── git-serve.lua ├── logger.lua ├── mime.lua ├── storage-fs.lua └── web-app.lua ├── main.lua ├── package.lua ├── server.lua ├── setup.sh └── test-main.lua /.gitignore: -------------------------------------------------------------------------------- 1 | rye 2 | *.git 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tim Caswell 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rye 2 | A git based publishing platform implemented in lua 3 | 4 | This is a work in progress. The goal is to re-implement the [wheaty](https://github.com/creationix/wheaty) platform in lua. 5 | 6 | So far it can render static sites directly from git repos on disk. 7 | 8 | ## Test it out! 9 | 10 | 1. Clone this repo. 11 | 2. Run the setup script. 12 | 3. [Install lit](https://github.com/luvit/lit#installing-lit). 13 | 4. Build rye using lit. 14 | 5. Run the new `rye` executable. 15 | 6. Open the test page in a browser. 16 | 17 | ```sh 18 | git clone https://github.com/creationix/rye.git 19 | cd rye 20 | ./setup.sh 21 | curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh 22 | ./lit make 23 | ./rye 24 | ``` 25 | -------------------------------------------------------------------------------- /deps/coro-channel.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/coro-channel" 2 | exports.version = "1.0.4" 3 | 4 | -- Given a raw uv_stream_t userdara, return coro-friendly read/write functions. 5 | -- Given a raw uv_stream_t userdara, return coro-friendly read/write functions. 6 | function exports.wrapStream(socket) 7 | local paused = true 8 | local queue = {} 9 | local waiting 10 | local reading = true 11 | local writing = true 12 | 13 | local onRead 14 | 15 | local function read() 16 | if #queue > 0 then 17 | return unpack(table.remove(queue, 1)) 18 | end 19 | if paused then 20 | paused = false 21 | assert(socket:read_start(onRead)) 22 | end 23 | waiting = coroutine.running() 24 | return coroutine.yield() 25 | end 26 | 27 | local flushing = false 28 | local flushed = false 29 | local function checkShutdown() 30 | if socket:is_closing() then return end 31 | if not flushing and not writing then 32 | flushing = true 33 | local thread = coroutine.running() 34 | socket:shutdown(function (err) 35 | flushed = true 36 | coroutine.resume(thread, not err, err) 37 | end) 38 | assert(coroutine.yield()) 39 | end 40 | if flushed and not reading then 41 | socket:close() 42 | end 43 | end 44 | 45 | function onRead(err, chunk) 46 | local data = err and {nil, err} or {chunk} 47 | if waiting then 48 | local thread = waiting 49 | waiting = nil 50 | assert(coroutine.resume(thread, unpack(data))) 51 | else 52 | queue[#queue + 1] = data 53 | if not paused then 54 | paused = true 55 | assert(socket:read_stop()) 56 | end 57 | end 58 | if not chunk then 59 | reading = false 60 | -- Close the whole socket if the writing side is also closed already. 61 | checkShutdown() 62 | end 63 | end 64 | 65 | local function write(chunk) 66 | if chunk == nil then 67 | -- Shutdown our side of the socket 68 | writing = false 69 | checkShutdown() 70 | else 71 | -- TODO: add backpressure by pausing and resuming coroutine 72 | -- when write buffer is full. 73 | assert(socket:write(chunk)) 74 | end 75 | end 76 | 77 | return read, write 78 | end 79 | 80 | 81 | function exports.chain(...) 82 | local args = {...} 83 | local nargs = select("#", ...) 84 | return function (read, write) 85 | local threads = {} -- coroutine thread for each item 86 | local waiting = {} -- flag when waiting to pull from upstream 87 | local boxes = {} -- storage when waiting to write to downstream 88 | for i = 1, nargs do 89 | threads[i] = coroutine.create(args[i]) 90 | waiting[i] = false 91 | local r, w 92 | if i == 1 then 93 | r = read 94 | else 95 | function r() 96 | local j = i - 1 97 | if boxes[j] then 98 | local data = boxes[j] 99 | boxes[j] = nil 100 | assert(coroutine.resume(threads[j])) 101 | return unpack(data) 102 | else 103 | waiting[i] = true 104 | return coroutine.yield() 105 | end 106 | end 107 | end 108 | if i == nargs then 109 | w = write 110 | else 111 | function w(...) 112 | local j = i + 1 113 | if waiting[j] then 114 | waiting[j] = false 115 | assert(coroutine.resume(threads[j], ...)) 116 | else 117 | boxes[i] = {...} 118 | coroutine.yield() 119 | end 120 | end 121 | end 122 | assert(coroutine.resume(threads[i], r, w)) 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /deps/coro-fs.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/coro-fs" 2 | exports.version = "1.2.3" 3 | 4 | local uv = require('uv') 5 | local fs = exports 6 | local pathJoin = require('luvi').path.join 7 | 8 | local function noop() end 9 | 10 | local function makeCallback() 11 | local thread = coroutine.running() 12 | return function (err, value, ...) 13 | if err then 14 | assert(coroutine.resume(thread, nil, err)) 15 | else 16 | assert(coroutine.resume(thread, value == nil and true or value, ...)) 17 | end 18 | end 19 | end 20 | 21 | function fs.mkdir(path, mode) 22 | uv.fs_mkdir(path, mode or 511, makeCallback()) 23 | return coroutine.yield() 24 | end 25 | function fs.open(path, flags, mode) 26 | uv.fs_open(path, flags or "r", mode or 438, makeCallback()) 27 | return coroutine.yield() 28 | end 29 | function fs.unlink(path) 30 | uv.fs_unlink(path, makeCallback()) 31 | return coroutine.yield() 32 | end 33 | function fs.stat(path) 34 | uv.fs_stat(path, makeCallback()) 35 | return coroutine.yield() 36 | end 37 | function fs.lstat(path) 38 | uv.fs_lstat(path, makeCallback()) 39 | return coroutine.yield() 40 | end 41 | function fs.symlink(target, path) 42 | uv.fs_symlink(target, path, makeCallback()) 43 | return coroutine.yield() 44 | end 45 | function fs.readlink(path) 46 | uv.fs_readlink(path, makeCallback()) 47 | return coroutine.yield() 48 | end 49 | function fs.fstat(fd) 50 | uv.fs_fstat(fd, makeCallback()) 51 | return coroutine.yield() 52 | end 53 | function fs.chmod(fd, path) 54 | uv.fs_chmod(fd, path, makeCallback()) 55 | return coroutine.yield() 56 | end 57 | function fs.fchmod(fd, mode) 58 | uv.fs_fchmod(fd, mode, makeCallback()) 59 | return coroutine.yield() 60 | end 61 | function fs.read(fd, length, offset) 62 | uv.fs_read(fd, length or 1024*48, offset or -1, makeCallback()) 63 | return coroutine.yield() 64 | end 65 | function fs.write(fd, data, offset) 66 | uv.fs_write(fd, data, offset or -1, makeCallback()) 67 | return coroutine.yield() 68 | end 69 | function fs.close(fd) 70 | uv.fs_close(fd, makeCallback()) 71 | return coroutine.yield() 72 | end 73 | function fs.access(path, flags) 74 | uv.fs_access(path, flags or "", makeCallback()) 75 | return coroutine.yield() 76 | end 77 | function fs.rename(path, newPath) 78 | uv.fs_rename(path, newPath, makeCallback()) 79 | return coroutine.yield() 80 | end 81 | function fs.rmdir(path) 82 | uv.fs_rmdir(path, makeCallback()) 83 | return coroutine.yield() 84 | end 85 | function fs.scandir(path) 86 | uv.fs_scandir(path, makeCallback()) 87 | local req, err = coroutine.yield() 88 | if not req then return nil, err end 89 | return function () 90 | return uv.fs_scandir_next(req) 91 | end 92 | end 93 | 94 | function fs.readFile(path) 95 | local fd, stat, data, err 96 | fd, err = fs.open(path) 97 | if err then return nil, err end 98 | stat, err = fs.fstat(fd) 99 | if stat then 100 | data, err = fs.read(fd, stat.size) 101 | end 102 | uv.fs_close(fd, noop) 103 | return data, err 104 | end 105 | 106 | function fs.writeFile(path, data, mkdir) 107 | local fd, success, err 108 | fd, err = fs.open(path, "w") 109 | if err then 110 | if mkdir and string.match(err, "^ENOENT:") then 111 | success, err = fs.mkdirp(pathJoin(path, "..")) 112 | if success then return fs.writeFile(path, data) end 113 | end 114 | return nil, err 115 | end 116 | success, err = fs.write(fd, data) 117 | uv.fs_close(fd, noop) 118 | return success, err 119 | end 120 | 121 | function fs.mkdirp(path, mode) 122 | local success, err = fs.mkdir(path, mode) 123 | if success or string.match(err, "^EEXIST") then 124 | return true 125 | end 126 | if string.match(err, "^ENOENT:") then 127 | success, err = fs.mkdirp(pathJoin(path, ".."), mode) 128 | if not success then return nil, err end 129 | return fs.mkdir(path, mode) 130 | end 131 | return nil, err 132 | end 133 | 134 | function fs.chroot(base) 135 | local chroot = { 136 | fstat = fs.fstat, 137 | fchmod = fs.fchmod, 138 | read = fs.read, 139 | write = fs.write, 140 | close = fs.close, 141 | } 142 | local function resolve(path) 143 | assert(path, "path missing") 144 | return pathJoin(base, pathJoin(path)) 145 | end 146 | function chroot.mkdir(path, mode) 147 | return fs.mkdir(resolve(path), mode) 148 | end 149 | function chroot.mkdirp(path, mode) 150 | return fs.mkdirp(resolve(path), mode) 151 | end 152 | function chroot.open(path, flags, mode) 153 | return fs.open(resolve(path), flags, mode) 154 | end 155 | function chroot.unlink(path) 156 | return fs.unlink(resolve(path)) 157 | end 158 | function chroot.stat(path) 159 | return fs.stat(resolve(path)) 160 | end 161 | function chroot.lstat(path) 162 | return fs.lstat(resolve(path)) 163 | end 164 | function chroot.symlink(target, path) 165 | -- TODO: should we resolve absolute target paths or treat it as opaque data? 166 | return fs.symlink(target, resolve(path)) 167 | end 168 | function chroot.readlink(path) 169 | return fs.readlink(resolve(path)) 170 | end 171 | function chroot.chmod(path, mode) 172 | return fs.chmod(resolve(path), mode) 173 | end 174 | function chroot.access(path, flags) 175 | return fs.access(resolve(path), flags) 176 | end 177 | function chroot.rename(path, newPath) 178 | return fs.rename(resolve(path), resolve(newPath)) 179 | end 180 | function chroot.rmdir(path) 181 | return fs.rmdir(resolve(path)) 182 | end 183 | function chroot.scandir(path, iter) 184 | return fs.scandir(resolve(path), iter) 185 | end 186 | function chroot.readFile(path) 187 | return fs.readFile(resolve(path)) 188 | end 189 | function chroot.writeFile(path, data, mkdir) 190 | return fs.writeFile(resolve(path), data, mkdir) 191 | end 192 | return chroot 193 | end 194 | -------------------------------------------------------------------------------- /deps/coro-tcp.lua: -------------------------------------------------------------------------------- 1 | 2 | exports.name = "creationix/coro-tcp" 3 | exports.version = "1.0.4" 4 | exports.dependencies = { 5 | "creationix/coro-channel@1.0.4" 6 | } 7 | 8 | local uv = require('uv') 9 | local wrapStream = require('coro-channel').wrapStream 10 | 11 | local function makeCallback() 12 | local thread = coroutine.running() 13 | return function (err, data) 14 | if err then 15 | return assert(coroutine.resume(thread, nil, err)) 16 | end 17 | return assert(coroutine.resume(thread, data or true)) 18 | end 19 | end 20 | exports.makeCallback = makeCallback 21 | 22 | function exports.connect(host, port) 23 | local res, success, err 24 | uv.getaddrinfo(host, port, { socktype = "stream", family="inet" }, makeCallback()) 25 | res, err = coroutine.yield() 26 | if not res then return nil, err end 27 | local socket = uv.new_tcp() 28 | socket:connect(res[1].addr, res[1].port, makeCallback()) 29 | success, err = coroutine.yield() 30 | if not success then return nil, err end 31 | local read, write = wrapStream(socket) 32 | return read, write, socket 33 | end 34 | 35 | function exports.createServer(addr, port, onConnect) 36 | local server = uv.new_tcp() 37 | assert(server:bind(addr, port)) 38 | server:listen(256, function (err) 39 | assert(not err, err) 40 | local socket = uv.new_tcp() 41 | server:accept(socket) 42 | coroutine.wrap(function () 43 | local success, failure = xpcall(function () 44 | local read, write = wrapStream(socket) 45 | return onConnect(read, write, socket) 46 | end, debug.traceback) 47 | if not success then 48 | print(failure) 49 | end 50 | socket:close() 51 | end)() 52 | end) 53 | end 54 | -------------------------------------------------------------------------------- /deps/coro-wrapper.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/coro-wrapper" 2 | exports.version = "0.1.0" 3 | 4 | function exports.reader(read, decode) 5 | local buffer = "" 6 | return function () 7 | while true do 8 | local item, extra = decode(buffer) 9 | if item then 10 | buffer = extra 11 | return item 12 | end 13 | local chunk = read() 14 | if not chunk then return end 15 | buffer = buffer .. chunk 16 | end 17 | end, 18 | function (newDecode) 19 | decode = newDecode 20 | end 21 | end 22 | 23 | function exports.writer(write, encode) 24 | return function (item) 25 | if not item then 26 | return write() 27 | end 28 | return write(encode(item)) 29 | end, 30 | function (newEncode) 31 | encode = newEncode 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /deps/git.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/git" 2 | exports.version = "0.1.1" 3 | 4 | local modes = { 5 | tree = 16384, -- 040000 6 | blob = 33188, -- 0100644 7 | exec = 33261, -- 0100755 8 | sym = 40960, -- 0120000 9 | commit = 57344, -- 0160000 10 | } 11 | modes.file = modes.blob 12 | exports.modes = modes 13 | 14 | function modes.isBlob(mode) 15 | -- (mode & 0140000) == 0100000 16 | return bit.band(mode, 49152) == 32768 17 | end 18 | 19 | function modes.isFile(mode) 20 | -- (mode & 0160000) === 0100000 21 | return bit.band(mode, 57344) == 32768 22 | end 23 | 24 | function modes.toType(mode) 25 | -- 0160000 26 | return mode == 57344 and "commit" 27 | -- 040000 28 | or mode == 16384 and "tree" 29 | -- (mode & 0140000) == 0100000 30 | or bit.band(mode, 49152) == 32768 and "blob" or 31 | "unknown" 32 | end 33 | 34 | local encoders = {} 35 | exports.encoders = encoders 36 | local decoders = {} 37 | exports.decoders = decoders 38 | 39 | 40 | local function treeSort(a, b) 41 | return ((a.mode == modes.tree) and (a.name .. "/") or a.name) 42 | < ((b.mode == modes.tree) and (b.name .. "/") or b.name) 43 | end 44 | 45 | -- Given two hex characters, return a single character 46 | local function hexToBin(cc) 47 | return string.char(tonumber(cc, 16)) 48 | end 49 | 50 | -- Given a raw character, return two hex characters 51 | local function binToHex(c) 52 | return string.format("%02x", string.byte(c, 1)) 53 | end 54 | 55 | -- Remove illegal characters in things like emails and names 56 | local function safe(text) 57 | return text:gsub("^[%.,:;\"']+", "") 58 | :gsub("[%.,:;\"']+$", "") 59 | :gsub("[%z\n<>]+", "") 60 | end 61 | exports.safe = safe 62 | 63 | local function formatDate(date) 64 | local seconds = date.seconds 65 | local offset = date.offset 66 | assert(type(seconds) == "number", "date.seconds must be number") 67 | assert(type(offset) == "number", "date.offset must be number") 68 | return string.format("%d %+03d%02d", seconds, offset / 60, offset % 60) 69 | end 70 | 71 | local function formatPerson(person) 72 | assert(type(person.name) == "string", "person.name must be string") 73 | assert(type(person.email) == "string", "person.email must be string") 74 | assert(type(person.date) == "table", "person.date must be table") 75 | return safe(person.name) .. 76 | " <" .. safe(person.email) .. "> " .. 77 | formatDate(person.date) 78 | end 79 | 80 | local function parsePerson(raw) 81 | local pattern = "^([^<]+) <([^>]+)> (%d+) ([-+])(%d%d)(%d%d)$" 82 | local name, email, seconds, sign, hours, minutes = string.match(raw, pattern) 83 | local offset = tonumber(hours) * 60 + tonumber(minutes) 84 | if sign == "-" then offset = -offset end 85 | return { 86 | name = name, 87 | email = email, 88 | { 89 | seconds = seconds, 90 | offset = offset 91 | } 92 | } 93 | end 94 | 95 | function encoders.blob(blob) 96 | assert(type(blob) == "string", "blobs must be strings") 97 | return blob 98 | end 99 | 100 | function decoders.blob(raw) 101 | return raw 102 | end 103 | 104 | function encoders.tree(tree) 105 | assert(type(tree) == "table", "trees must be tables") 106 | local parts = {} 107 | for i = 1, #tree do 108 | local value = tree[i] 109 | assert(type(value.name) == "string", "tree entry name must be string") 110 | assert(type(value.mode) == "number", "tree entry mode must be number") 111 | assert(type(value.hash) == "string", "tree entry hash must be string") 112 | parts[#parts + 1] = { 113 | name = value.name, 114 | mode = value.mode, 115 | hash = value.hash, 116 | } 117 | end 118 | table.sort(parts, treeSort) 119 | for i = 1, #parts do 120 | local entry = parts[i] 121 | parts[i] = string.format("%o %s\0", 122 | entry.mode, 123 | entry.name 124 | ) .. string.gsub(entry.hash, "..", hexToBin) 125 | end 126 | return table.concat(parts) 127 | end 128 | 129 | function decoders.tree(raw) 130 | local start, length = 1, #raw 131 | local tree = {} 132 | while start <= length do 133 | local pattern = "^([0-7]+) ([^%z]+)%z(....................)" 134 | local s, e, mode, name, hash = string.find(raw, pattern, start) 135 | assert(s, "Problem parsing tree") 136 | hash = string.gsub(hash, ".", binToHex) 137 | mode = tonumber(mode, 8) 138 | tree[#tree + 1] = { 139 | name = name, 140 | mode = mode, 141 | hash = hash 142 | } 143 | start = e + 1 144 | end 145 | return tree 146 | end 147 | 148 | function encoders.tag(tag) 149 | assert(type(tag) == "table", "annotated tags must be tables") 150 | assert(type(tag.object) == "string", "tag.object must be hash string") 151 | assert(type(tag.type) == "string", "tag.type must be string") 152 | assert(type(tag.tag) == "string", "tag.tag must be string") 153 | assert(type(tag.tagger) == "table", "tag.tagger must be table") 154 | assert(type(tag.message) == "string", "tag.message must be string") 155 | if tag.message[#tag.message] ~= "\n" then 156 | tag.message = tag.message .. "\n" 157 | end 158 | return string.format( 159 | "object %s\ntype %s\ntag %s\ntagger %s\n\n%s", 160 | tag.object, tag.type, tag.tag, formatPerson(tag.tagger), tag.message) 161 | end 162 | 163 | function decoders.tag(raw) 164 | local s, _, message = string.find(raw, "\n\n(.*)$", 1) 165 | raw = string.sub(raw, 1, s) 166 | local start = 1 167 | local pattern = "^([^ ]+) ([^\n]+)\n" 168 | local data = {message=message} 169 | while true do 170 | local s, e, name, value = string.find(raw, pattern, start) 171 | if not s then break end 172 | if name == "tagger" then 173 | value = parsePerson(value) 174 | end 175 | data[name] = value 176 | start = e + 1 177 | end 178 | return data 179 | end 180 | 181 | 182 | function encoders.commit(commit) 183 | assert(type(commit) == "table", "commits must be tables") 184 | assert(type(commit.tree) == "string", "commit.tree must be hash string") 185 | assert(type(commit.parents) == "table", "commit.parents must be table") 186 | assert(type(commit.author) == "table", "commit.author must be table") 187 | assert(type(commit.committer) == "table", "commit.committer must be table") 188 | assert(type(commit.message) == "string", "commit.message must be string") 189 | local parents = {} 190 | for i = 1, #commit.parents do 191 | local parent = commit.parents[i] 192 | assert(type(parent) == "string", "commit.parents must be hash strings") 193 | parents[i] = string.format("parent %s\n", parent) 194 | end 195 | return string.format( 196 | "tree %s\n%sauthor %s\ncommitter %s\n\n%s", 197 | commit.tree, table.concat(parents), formatPerson(commit.author), 198 | formatPerson(commit.committer), commit.message) 199 | end 200 | 201 | function decoders.commit(raw) 202 | local s, _, message = string.find(raw, "\n\n(.*)$", 1) 203 | raw = string.sub(raw, 1, s) 204 | local start = 1 205 | local pattern = "^([^ ]+) ([^\n]+)\n" 206 | local parents = {} 207 | local data = {message=message,parents=parents} 208 | while true do 209 | local s, e, name, value = string.find(raw, pattern, start) 210 | if not s then break end 211 | if name == "author" or name == "committer" then 212 | value = parsePerson(value) 213 | end 214 | if name == "parent" then 215 | parents[#parents + 1] = value 216 | else 217 | data[name] = value 218 | end 219 | start = e + 1 220 | end 221 | return data 222 | end 223 | 224 | function exports.frame(kind, data) 225 | assert(type(data) == "string", "data must be pre-encoded string") 226 | data = string.format("%s %d\0", kind, #data) .. data 227 | return data 228 | end 229 | 230 | function exports.deframe(raw) 231 | local pattern = "^([^ ]+) (%d+)%z(.*)$" 232 | local kind, size, body = string.match(raw, pattern) 233 | assert(kind, "Problem parsing framed git data") 234 | size = tonumber(size) 235 | assert(size == #body, "Body size mismatch") 236 | return kind, body 237 | end 238 | 239 | function exports.listToMap(list) 240 | local map = {} 241 | for i = 1, #list do 242 | local entry = list[i] 243 | map[entry.name] = {mode = entry.mode, hash = entry.hash} 244 | end 245 | return map 246 | end 247 | 248 | function exports.mapToList(map) 249 | local list = {} 250 | for name, entry in pairs(map) do 251 | list[#list + 1] = {name = name, mode = entry.mode, hash = entry.hash} 252 | end 253 | return list 254 | end 255 | -------------------------------------------------------------------------------- /deps/hex-bin.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/hex-bin" 2 | exports.version = "1.0.0" 3 | 4 | local function binToHex(c) 5 | return string.format("%02x", string.byte(c, 1)) 6 | end 7 | 8 | exports.binToHex = function(bin) 9 | local hex = string.gsub(bin, ".", binToHex) 10 | return hex 11 | end 12 | 13 | local function hexToBin(cc) 14 | return string.char(tonumber(cc, 16)) 15 | end 16 | 17 | exports.hexToBin = function (hex) 18 | local bin = string.gsub(hex, "..", hexToBin) 19 | return bin 20 | end 21 | -------------------------------------------------------------------------------- /deps/http-codec.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2014-2015 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | 19 | exports.name = "luvit/http-codec" 20 | exports.version = "0.1.4" 21 | 22 | local sub = string.sub 23 | local gsub = string.gsub 24 | local lower = string.lower 25 | local find = string.find 26 | local format = string.format 27 | local concat = table.concat 28 | local match = string.match 29 | 30 | local STATUS_CODES = { 31 | [100] = 'Continue', 32 | [101] = 'Switching Protocols', 33 | [102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918 34 | [200] = 'OK', 35 | [201] = 'Created', 36 | [202] = 'Accepted', 37 | [203] = 'Non-Authoritative Information', 38 | [204] = 'No Content', 39 | [205] = 'Reset Content', 40 | [206] = 'Partial Content', 41 | [207] = 'Multi-Status', -- RFC 4918 42 | [300] = 'Multiple Choices', 43 | [301] = 'Moved Permanently', 44 | [302] = 'Moved Temporarily', 45 | [303] = 'See Other', 46 | [304] = 'Not Modified', 47 | [305] = 'Use Proxy', 48 | [307] = 'Temporary Redirect', 49 | [400] = 'Bad Request', 50 | [401] = 'Unauthorized', 51 | [402] = 'Payment Required', 52 | [403] = 'Forbidden', 53 | [404] = 'Not Found', 54 | [405] = 'Method Not Allowed', 55 | [406] = 'Not Acceptable', 56 | [407] = 'Proxy Authentication Required', 57 | [408] = 'Request Time-out', 58 | [409] = 'Conflict', 59 | [410] = 'Gone', 60 | [411] = 'Length Required', 61 | [412] = 'Precondition Failed', 62 | [413] = 'Request Entity Too Large', 63 | [414] = 'Request-URI Too Large', 64 | [415] = 'Unsupported Media Type', 65 | [416] = 'Requested Range Not Satisfiable', 66 | [417] = 'Expectation Failed', 67 | [418] = "I'm a teapot", -- RFC 2324 68 | [422] = 'Unprocessable Entity', -- RFC 4918 69 | [423] = 'Locked', -- RFC 4918 70 | [424] = 'Failed Dependency', -- RFC 4918 71 | [425] = 'Unordered Collection', -- RFC 4918 72 | [426] = 'Upgrade Required', -- RFC 2817 73 | [500] = 'Internal Server Error', 74 | [501] = 'Not Implemented', 75 | [502] = 'Bad Gateway', 76 | [503] = 'Service Unavailable', 77 | [504] = 'Gateway Time-out', 78 | [505] = 'HTTP Version not supported', 79 | [506] = 'Variant Also Negotiates', -- RFC 2295 80 | [507] = 'Insufficient Storage', -- RFC 4918 81 | [509] = 'Bandwidth Limit Exceeded', 82 | [510] = 'Not Extended' -- RFC 2774 83 | } 84 | 85 | exports.encoder = function () 86 | 87 | local mode 88 | local encodeHead, encodeRaw, encodeChunked 89 | 90 | function encodeHead(item) 91 | if not item or item == "" then 92 | return item 93 | elseif not (type(item) == "table") then 94 | error("expected a table but got a " .. type(item) .. " when encoding data") 95 | end 96 | local head, chunkedEncoding 97 | local version = item.version or 1.1 98 | if item.method then 99 | local path = item.path 100 | assert(path and #path > 0, "expected non-empty path") 101 | head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' } 102 | else 103 | local reason = item.reason or STATUS_CODES[item.code] 104 | head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' } 105 | end 106 | for i = 1, #item do 107 | local key, value = unpack(item[i]) 108 | local lowerKey = lower(key) 109 | if lowerKey == "transfer-encoding" then 110 | chunkedEncoding = lower(value) == "chunked" 111 | end 112 | value = gsub(tostring(value), "[\r\n]+", " ") 113 | head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n' 114 | end 115 | head[#head + 1] = '\r\n' 116 | 117 | mode = chunkedEncoding and encodeChunked or encodeRaw 118 | return concat(head) 119 | end 120 | 121 | function encodeRaw(item) 122 | if type(item) ~= "string" then 123 | mode = encodeHead 124 | return encodeHead(item) 125 | end 126 | return item 127 | end 128 | 129 | function encodeChunked(item) 130 | if type(item) ~= "string" then 131 | mode = encodeHead 132 | local extra = encodeHead(item) 133 | if extra then 134 | return "0\r\n\r\n" .. extra 135 | else 136 | return "0\r\n\r\n" 137 | end 138 | end 139 | if #item == 0 then 140 | mode = encodeHead 141 | end 142 | return format("%x", #item) .. "\r\n" .. item .. "\r\n" 143 | end 144 | 145 | mode = encodeHead 146 | return function (item) 147 | return mode(item) 148 | end 149 | end 150 | 151 | exports.decoder = function () 152 | 153 | -- This decoder is somewhat stateful with 5 different parsing states. 154 | local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted 155 | local mode -- state variable that points to various decoders 156 | local bytesLeft -- For counted decoder 157 | 158 | -- This state is for decoding the status line and headers. 159 | function decodeHead(chunk) 160 | if not chunk then return end 161 | 162 | local _, length = find(chunk, "\r?\n\r?\n", 1) 163 | -- First make sure we have all the head before continuing 164 | if not length then 165 | if #chunk < 8 * 1024 then return end 166 | -- But protect against evil clients by refusing heads over 8K long. 167 | error("entity too large") 168 | end 169 | 170 | -- Parse the status/request line 171 | local head = {} 172 | local _, offset 173 | local version 174 | _, offset, version, head.code, head.reason = 175 | find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]+)\r?\n") 176 | if offset then 177 | head.code = tonumber(head.code) 178 | else 179 | _, offset, head.method, head.path, version = 180 | find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n") 181 | if not offset then 182 | error("expected HTTP data") 183 | end 184 | end 185 | version = tonumber(version) 186 | head.version = version 187 | head.keepAlive = version > 1.0 188 | 189 | -- We need to inspect some headers to know how to parse the body. 190 | local contentLength 191 | local chunkedEncoding 192 | 193 | -- Parse the header lines 194 | while true do 195 | local key, value 196 | _, offset, key, value = find(chunk, "^([^:]+): *([^\r\n]+)\r?\n", offset + 1) 197 | if not offset then break end 198 | local lowerKey = lower(key) 199 | 200 | -- Inspect a few headers and remember the values 201 | if lowerKey == "content-length" then 202 | contentLength = tonumber(value) 203 | elseif lowerKey == "transfer-encoding" then 204 | chunkedEncoding = lower(value) == "chunked" 205 | elseif lowerKey == "connection" then 206 | head.keepAlive = lower(value) == "keep-alive" 207 | end 208 | head[#head + 1] = {key, value} 209 | end 210 | 211 | if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0))) 212 | or (head.method == "GET" or head.method == "HEAD") then 213 | mode = decodeEmpty 214 | elseif chunkedEncoding then 215 | mode = decodeChunked 216 | elseif contentLength then 217 | bytesLeft = contentLength 218 | mode = decodeCounted 219 | elseif not head.keepAlive then 220 | mode = decodeRaw 221 | end 222 | 223 | return head, sub(chunk, length + 1) 224 | 225 | end 226 | 227 | -- This is used for inserting a single empty string into the output string for known empty bodies 228 | function decodeEmpty(chunk) 229 | mode = decodeHead 230 | return "", chunk or "" 231 | end 232 | 233 | function decodeRaw(chunk) 234 | if not chunk then return "", "" end 235 | if #chunk == 0 then return end 236 | return chunk, "" 237 | end 238 | 239 | function decodeChunked(chunk) 240 | local len, term 241 | len, term = match(chunk, "^(%x+)(..)") 242 | if not len then return end 243 | assert(term == "\r\n") 244 | local length = tonumber(len, 16) 245 | if #chunk < length + 4 + #len then return end 246 | if length == 0 then 247 | mode = decodeHead 248 | end 249 | chunk = sub(chunk, #len + 3) 250 | assert(sub(chunk, length + 1, length + 2) == "\r\n") 251 | return sub(chunk, 1, length), sub(chunk, length + 3) 252 | end 253 | 254 | function decodeCounted(chunk) 255 | if bytesLeft == 0 then 256 | mode = decodeEmpty 257 | return mode(chunk) 258 | end 259 | local length = #chunk 260 | -- Make sure we have at least one byte to process 261 | if length == 0 then return end 262 | 263 | if length >= bytesLeft then 264 | mode = decodeEmpty 265 | end 266 | 267 | -- If the entire chunk fits, pass it all through 268 | if length <= bytesLeft then 269 | bytesLeft = bytesLeft - length 270 | return chunk, "" 271 | end 272 | 273 | return sub(chunk, 1, bytesLeft), sub(chunk, bytesLeft + 1) 274 | end 275 | 276 | -- Switch between states by changing which decoder mode points to 277 | mode = decodeHead 278 | return function (chunk) 279 | return mode(chunk) 280 | end 281 | 282 | end 283 | -------------------------------------------------------------------------------- /deps/json.lua: -------------------------------------------------------------------------------- 1 | exports.name = "luvit/json" 2 | exports.version = "0.1.0" 3 | 4 | -- Module options: 5 | local always_try_using_lpeg = false 6 | local register_global_module_table = false 7 | local global_module_name = 'json' 8 | 9 | --[==[ 10 | 11 | David Kolf's JSON module for Lua 5.1/5.2 12 | 13 | Version 2.5 14 | 15 | 16 | For the documentation see the corresponding readme.txt or visit 17 | . 18 | 19 | You can contact the author by sending an e-mail to 'david' at the 20 | domain 'dkolf.de'. 21 | 22 | 23 | Copyright (C) 2010-2013 David Heiko Kolf 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining 26 | a copy of this software and associated documentation files (the 27 | "Software"), to deal in the Software without restriction, including 28 | without limitation the rights to use, copy, modify, merge, publish, 29 | distribute, sublicense, and/or sell copies of the Software, and to 30 | permit persons to whom the Software is furnished to do so, subject to 31 | the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be 34 | included in all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 37 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 38 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 39 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 40 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 41 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 42 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | 45 | --]==] 46 | 47 | -- global dependencies: 48 | local pairs, type, tostring, tonumber, getmetatable, setmetatable = 49 | pairs, type, tostring, tonumber, getmetatable, setmetatable 50 | local error, require, pcall, select = error, require, pcall, select 51 | local floor, huge = math.floor, math.huge 52 | local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = 53 | string.rep, string.gsub, string.sub, string.byte, string.char, 54 | string.find, string.len, string.format 55 | local strmatch = string.match 56 | local concat = table.concat 57 | 58 | local json = exports 59 | json.original_version = "dkjson 2.5" 60 | 61 | if register_global_module_table then 62 | _G[global_module_name] = json 63 | end 64 | 65 | _ENV = nil -- blocking globals in Lua 5.2 66 | 67 | pcall (function() 68 | -- Enable access to blocked metatables. 69 | -- Don't worry, this module doesn't change anything in them. 70 | local debmeta = require "debug".getmetatable 71 | if debmeta then getmetatable = debmeta end 72 | end) 73 | 74 | json.null = setmetatable ({}, { 75 | __tojson = function () return "null" end 76 | }) 77 | 78 | local function isarray (tbl) 79 | local max, n, arraylen = 0, 0, 0 80 | for k,v in pairs (tbl) do 81 | if k == 'n' and type(v) == 'number' then 82 | arraylen = v 83 | if v > max then 84 | max = v 85 | end 86 | else 87 | if type(k) ~= 'number' or k < 1 or floor(k) ~= k then 88 | return false 89 | end 90 | if k > max then 91 | max = k 92 | end 93 | n = n + 1 94 | end 95 | end 96 | if max > 10 and max > arraylen and max > n * 2 then 97 | return false -- don't create an array with too many holes 98 | end 99 | return true, max 100 | end 101 | 102 | local escapecodes = { 103 | ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", 104 | ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" 105 | } 106 | 107 | local function escapeutf8 (uchar) 108 | local value = escapecodes[uchar] 109 | if value then 110 | return value 111 | end 112 | local a, b, c, d = strbyte (uchar, 1, 4) 113 | a, b, c, d = a or 0, b or 0, c or 0, d or 0 114 | if a <= 0x7f then 115 | value = a 116 | elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then 117 | value = (a - 0xc0) * 0x40 + b - 0x80 118 | elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then 119 | value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 120 | elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then 121 | value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 122 | else 123 | return "" 124 | end 125 | if value <= 0xffff then 126 | return strformat ("\\u%.4x", value) 127 | elseif value <= 0x10ffff then 128 | -- encode as UTF-16 surrogate pair 129 | value = value - 0x10000 130 | local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) 131 | return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) 132 | else 133 | return "" 134 | end 135 | end 136 | 137 | local function fsub (str, pattern, repl) 138 | -- gsub always builds a new string in a buffer, even when no match 139 | -- exists. First using find should be more efficient when most strings 140 | -- don't contain the pattern. 141 | if strfind (str, pattern) then 142 | return gsub (str, pattern, repl) 143 | else 144 | return str 145 | end 146 | end 147 | 148 | local function quotestring (value) 149 | -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js 150 | value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) 151 | if strfind (value, "[\194\216\220\225\226\239]") then 152 | value = fsub (value, "\194[\128-\159\173]", escapeutf8) 153 | value = fsub (value, "\216[\128-\132]", escapeutf8) 154 | value = fsub (value, "\220\143", escapeutf8) 155 | value = fsub (value, "\225\158[\180\181]", escapeutf8) 156 | value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) 157 | value = fsub (value, "\226\129[\160-\175]", escapeutf8) 158 | value = fsub (value, "\239\187\191", escapeutf8) 159 | value = fsub (value, "\239\191[\176-\191]", escapeutf8) 160 | end 161 | return "\"" .. value .. "\"" 162 | end 163 | json.quotestring = quotestring 164 | 165 | local function replace(str, o, n) 166 | local i, j = strfind (str, o, 1, true) 167 | if i then 168 | return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) 169 | else 170 | return str 171 | end 172 | end 173 | 174 | -- locale independent num2str and str2num functions 175 | local decpoint, numfilter 176 | 177 | local function updatedecpoint () 178 | decpoint = strmatch(tostring(0.5), "([^05+])") 179 | -- build a filter that can be used to remove group separators 180 | numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" 181 | end 182 | 183 | updatedecpoint() 184 | 185 | local function num2str (num) 186 | return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") 187 | end 188 | 189 | local function str2num (str) 190 | local num = tonumber(replace(str, ".", decpoint)) 191 | if not num then 192 | updatedecpoint() 193 | num = tonumber(replace(str, ".", decpoint)) 194 | end 195 | return num 196 | end 197 | 198 | local function addnewline2 (level, buffer, buflen) 199 | buffer[buflen+1] = "\n" 200 | buffer[buflen+2] = strrep (" ", level) 201 | buflen = buflen + 2 202 | return buflen 203 | end 204 | 205 | function json.addnewline (state) 206 | if state.indent then 207 | state.bufferlen = addnewline2 (state.level or 0, 208 | state.buffer, state.bufferlen or #(state.buffer)) 209 | end 210 | end 211 | 212 | local encode2 -- forward declaration 213 | 214 | local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) 215 | local kt = type (key) 216 | if kt ~= 'string' and kt ~= 'number' then 217 | return nil, "type '" .. kt .. "' is not supported as a key by JSON." 218 | end 219 | if prev then 220 | buflen = buflen + 1 221 | buffer[buflen] = "," 222 | end 223 | if indent then 224 | buflen = addnewline2 (level, buffer, buflen) 225 | end 226 | buffer[buflen+1] = quotestring (key) 227 | buffer[buflen+2] = ":" 228 | return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) 229 | end 230 | 231 | local function appendcustom(res, buffer, state) 232 | local buflen = state.bufferlen 233 | if type (res) == 'string' then 234 | buflen = buflen + 1 235 | buffer[buflen] = res 236 | end 237 | return buflen 238 | end 239 | 240 | local function exception(reason, value, state, buffer, buflen, defaultmessage) 241 | defaultmessage = defaultmessage or reason 242 | local handler = state.exception 243 | if not handler then 244 | return nil, defaultmessage 245 | else 246 | state.bufferlen = buflen 247 | local ret, msg = handler (reason, value, state, defaultmessage) 248 | if not ret then return nil, msg or defaultmessage end 249 | return appendcustom(ret, buffer, state) 250 | end 251 | end 252 | 253 | function json.encodeexception(reason, value, state, defaultmessage) 254 | return quotestring("<" .. defaultmessage .. ">") 255 | end 256 | 257 | encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) 258 | local valtype = type (value) 259 | local valmeta = getmetatable (value) 260 | valmeta = type (valmeta) == 'table' and valmeta -- only tables 261 | local valtojson = valmeta and valmeta.__tojson 262 | if valtojson then 263 | if tables[value] then 264 | return exception('reference cycle', value, state, buffer, buflen) 265 | end 266 | tables[value] = true 267 | state.bufferlen = buflen 268 | local ret, msg = valtojson (value, state) 269 | if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end 270 | tables[value] = nil 271 | buflen = appendcustom(ret, buffer, state) 272 | elseif value == nil then 273 | buflen = buflen + 1 274 | buffer[buflen] = "null" 275 | elseif valtype == 'number' then 276 | local s 277 | if value ~= value or value >= huge or -value >= huge then 278 | -- This is the behaviour of the original JSON implementation. 279 | s = "null" 280 | else 281 | s = num2str (value) 282 | end 283 | buflen = buflen + 1 284 | buffer[buflen] = s 285 | elseif valtype == 'boolean' then 286 | buflen = buflen + 1 287 | buffer[buflen] = value and "true" or "false" 288 | elseif valtype == 'string' then 289 | buflen = buflen + 1 290 | buffer[buflen] = quotestring (value) 291 | elseif valtype == 'table' then 292 | if tables[value] then 293 | return exception('reference cycle', value, state, buffer, buflen) 294 | end 295 | tables[value] = true 296 | level = level + 1 297 | local isa, n = isarray (value) 298 | if n == 0 and valmeta and valmeta.__jsontype == 'object' then 299 | isa = false 300 | end 301 | local msg 302 | if isa then -- JSON array 303 | buflen = buflen + 1 304 | buffer[buflen] = "[" 305 | for i = 1, n do 306 | buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) 307 | if not buflen then return nil, msg end 308 | if i < n then 309 | buflen = buflen + 1 310 | buffer[buflen] = "," 311 | end 312 | end 313 | buflen = buflen + 1 314 | buffer[buflen] = "]" 315 | else -- JSON object 316 | local prev = false 317 | buflen = buflen + 1 318 | buffer[buflen] = "{" 319 | local order = valmeta and valmeta.__jsonorder or globalorder 320 | if order then 321 | local used = {} 322 | n = #order 323 | for i = 1, n do 324 | local k = order[i] 325 | local v = value[k] 326 | local _ 327 | if v then 328 | used[k] = true 329 | buflen, _ = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 330 | prev = true -- add a seperator before the next element 331 | end 332 | end 333 | for k,v in pairs (value) do 334 | if not used[k] then 335 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 336 | if not buflen then return nil, msg end 337 | prev = true -- add a seperator before the next element 338 | end 339 | end 340 | else -- unordered 341 | for k,v in pairs (value) do 342 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) 343 | if not buflen then return nil, msg end 344 | prev = true -- add a seperator before the next element 345 | end 346 | end 347 | if indent then 348 | buflen = addnewline2 (level - 1, buffer, buflen) 349 | end 350 | buflen = buflen + 1 351 | buffer[buflen] = "}" 352 | end 353 | tables[value] = nil 354 | else 355 | return exception ('unsupported type', value, state, buffer, buflen, 356 | "type '" .. valtype .. "' is not supported by JSON.") 357 | end 358 | return buflen 359 | end 360 | 361 | function json.encode (value, state) 362 | state = state or {} 363 | local oldbuffer = state.buffer 364 | local buffer = oldbuffer or {} 365 | state.buffer = buffer 366 | updatedecpoint() 367 | local ret, msg = encode2 (value, state.indent, state.level or 0, 368 | buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) 369 | if not ret then 370 | error (msg, 2) 371 | elseif oldbuffer == buffer then 372 | state.bufferlen = ret 373 | return true 374 | else 375 | state.bufferlen = nil 376 | state.buffer = nil 377 | return concat (buffer) 378 | end 379 | end 380 | 381 | local function loc (str, where) 382 | local line, pos, linepos = 1, 1, 0 383 | while true do 384 | pos = strfind (str, "\n", pos, true) 385 | if pos and pos < where then 386 | line = line + 1 387 | linepos = pos 388 | pos = pos + 1 389 | else 390 | break 391 | end 392 | end 393 | return "line " .. line .. ", column " .. (where - linepos) 394 | end 395 | 396 | local function unterminated (str, what, where) 397 | return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) 398 | end 399 | 400 | local function scanwhite (str, pos) 401 | while true do 402 | pos = strfind (str, "%S", pos) 403 | if not pos then return nil end 404 | local sub2 = strsub (str, pos, pos + 1) 405 | if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then 406 | -- UTF-8 Byte Order Mark 407 | pos = pos + 3 408 | elseif sub2 == "//" then 409 | pos = strfind (str, "[\n\r]", pos + 2) 410 | if not pos then return nil end 411 | elseif sub2 == "/*" then 412 | pos = strfind (str, "*/", pos + 2) 413 | if not pos then return nil end 414 | pos = pos + 2 415 | else 416 | return pos 417 | end 418 | end 419 | end 420 | 421 | local escapechars = { 422 | ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", 423 | ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" 424 | } 425 | 426 | local function unichar (value) 427 | if value < 0 then 428 | return nil 429 | elseif value <= 0x007f then 430 | return strchar (value) 431 | elseif value <= 0x07ff then 432 | return strchar (0xc0 + floor(value/0x40), 433 | 0x80 + (floor(value) % 0x40)) 434 | elseif value <= 0xffff then 435 | return strchar (0xe0 + floor(value/0x1000), 436 | 0x80 + (floor(value/0x40) % 0x40), 437 | 0x80 + (floor(value) % 0x40)) 438 | elseif value <= 0x10ffff then 439 | return strchar (0xf0 + floor(value/0x40000), 440 | 0x80 + (floor(value/0x1000) % 0x40), 441 | 0x80 + (floor(value/0x40) % 0x40), 442 | 0x80 + (floor(value) % 0x40)) 443 | else 444 | return nil 445 | end 446 | end 447 | 448 | local function scanstring (str, pos) 449 | local lastpos = pos + 1 450 | local buffer, n = {}, 0 451 | while true do 452 | local nextpos = strfind (str, "[\"\\]", lastpos) 453 | if not nextpos then 454 | return unterminated (str, "string", pos) 455 | end 456 | if nextpos > lastpos then 457 | n = n + 1 458 | buffer[n] = strsub (str, lastpos, nextpos - 1) 459 | end 460 | if strsub (str, nextpos, nextpos) == "\"" then 461 | lastpos = nextpos + 1 462 | break 463 | else 464 | local escchar = strsub (str, nextpos + 1, nextpos + 1) 465 | local value 466 | if escchar == "u" then 467 | value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) 468 | if value then 469 | local value2 470 | if 0xD800 <= value and value <= 0xDBff then 471 | -- we have the high surrogate of UTF-16. Check if there is a 472 | -- low surrogate escaped nearby to combine them. 473 | if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then 474 | value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) 475 | if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then 476 | value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 477 | else 478 | value2 = nil -- in case it was out of range for a low surrogate 479 | end 480 | end 481 | end 482 | value = value and unichar (value) 483 | if value then 484 | if value2 then 485 | lastpos = nextpos + 12 486 | else 487 | lastpos = nextpos + 6 488 | end 489 | end 490 | end 491 | end 492 | if not value then 493 | value = escapechars[escchar] or escchar 494 | lastpos = nextpos + 2 495 | end 496 | n = n + 1 497 | buffer[n] = value 498 | end 499 | end 500 | if n == 1 then 501 | return buffer[1], lastpos 502 | elseif n > 1 then 503 | return concat (buffer), lastpos 504 | else 505 | return "", lastpos 506 | end 507 | end 508 | 509 | local scanvalue -- forward declaration 510 | 511 | local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) 512 | local tbl, n = {}, 0 513 | local pos = startpos + 1 514 | if what == 'object' then 515 | setmetatable (tbl, objectmeta) 516 | else 517 | setmetatable (tbl, arraymeta) 518 | end 519 | while true do 520 | pos = scanwhite (str, pos) 521 | if not pos then return unterminated (str, what, startpos) end 522 | local char = strsub (str, pos, pos) 523 | if char == closechar then 524 | return tbl, pos + 1 525 | end 526 | local val1, err 527 | val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) 528 | if err then return nil, pos, err end 529 | pos = scanwhite (str, pos) 530 | if not pos then return unterminated (str, what, startpos) end 531 | char = strsub (str, pos, pos) 532 | if char == ":" then 533 | if val1 == nil then 534 | return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" 535 | end 536 | pos = scanwhite (str, pos + 1) 537 | if not pos then return unterminated (str, what, startpos) end 538 | local val2 539 | val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) 540 | if err then return nil, pos, err end 541 | tbl[val1] = val2 542 | pos = scanwhite (str, pos) 543 | if not pos then return unterminated (str, what, startpos) end 544 | char = strsub (str, pos, pos) 545 | else 546 | n = n + 1 547 | tbl[n] = val1 548 | end 549 | if char == "," then 550 | pos = pos + 1 551 | end 552 | end 553 | end 554 | 555 | scanvalue = function (str, pos, nullval, objectmeta, arraymeta) 556 | pos = pos or 1 557 | pos = scanwhite (str, pos) 558 | if not pos then 559 | return nil, strlen (str) + 1, "no valid JSON value (reached the end)" 560 | end 561 | local char = strsub (str, pos, pos) 562 | if char == "{" then 563 | return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) 564 | elseif char == "[" then 565 | return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) 566 | elseif char == "\"" then 567 | return scanstring (str, pos) 568 | else 569 | local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) 570 | if pstart then 571 | local number = str2num (strsub (str, pstart, pend)) 572 | if number then 573 | return number, pend + 1 574 | end 575 | end 576 | pstart, pend = strfind (str, "^%a%w*", pos) 577 | if pstart then 578 | local name = strsub (str, pstart, pend) 579 | if name == "true" then 580 | return true, pend + 1 581 | elseif name == "false" then 582 | return false, pend + 1 583 | elseif name == "null" then 584 | return nullval, pend + 1 585 | end 586 | end 587 | return nil, pos, "no valid JSON value at " .. loc (str, pos) 588 | end 589 | end 590 | 591 | local function optionalmetatables(...) 592 | if select("#", ...) > 0 then 593 | return ... 594 | else 595 | return {__jsontype = 'object'}, {__jsontype = 'array'} 596 | end 597 | end 598 | 599 | function json.decode (str, pos, nullval, ...) 600 | local objectmeta, arraymeta = optionalmetatables(...) 601 | return scanvalue (str, pos, nullval, objectmeta, arraymeta) 602 | end 603 | 604 | function json.use_lpeg () 605 | local g = require ("lpeg") 606 | 607 | if g.version() == "0.11" then 608 | error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" 609 | end 610 | 611 | local pegmatch = g.match 612 | local P, S, R = g.P, g.S, g.R 613 | 614 | local function ErrorCall (str, pos, msg, state) 615 | if not state.msg then 616 | state.msg = msg .. " at " .. loc (str, pos) 617 | state.pos = pos 618 | end 619 | return false 620 | end 621 | 622 | local function Err (msg) 623 | return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) 624 | end 625 | 626 | local SingleLineComment = P"//" * (1 - S"\n\r")^0 627 | local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" 628 | local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 629 | 630 | local PlainChar = 1 - S"\"\\\n\r" 631 | local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars 632 | local HexDigit = R("09", "af", "AF") 633 | local function UTF16Surrogate (match, pos, high, low) 634 | high, low = tonumber (high, 16), tonumber (low, 16) 635 | if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then 636 | return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) 637 | else 638 | return false 639 | end 640 | end 641 | local function UTF16BMP (hex) 642 | return unichar (tonumber (hex, 16)) 643 | end 644 | local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) 645 | local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP 646 | local Char = UnicodeEscape + EscapeSequence + PlainChar 647 | local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string") 648 | local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) 649 | local Fractal = P"." * R"09"^0 650 | local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 651 | local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num 652 | local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) 653 | local SimpleValue = Number + String + Constant 654 | local ArrayContent, ObjectContent 655 | 656 | -- The functions parsearray and parseobject parse only a single value/pair 657 | -- at a time and store them directly to avoid hitting the LPeg limits. 658 | local function parsearray (str, pos, nullval, state) 659 | local obj, cont 660 | local npos 661 | local t, nt = {}, 0 662 | repeat 663 | obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) 664 | if not npos then break end 665 | pos = npos 666 | nt = nt + 1 667 | t[nt] = obj 668 | until cont == 'last' 669 | return pos, setmetatable (t, state.arraymeta) 670 | end 671 | 672 | local function parseobject (str, pos, nullval, state) 673 | local obj, key, cont 674 | local npos 675 | local t = {} 676 | repeat 677 | key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) 678 | if not npos then break end 679 | pos = npos 680 | t[key] = obj 681 | until cont == 'last' 682 | return pos, setmetatable (t, state.objectmeta) 683 | end 684 | 685 | local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected") 686 | local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected") 687 | local Value = Space * (Array + Object + SimpleValue) 688 | local ExpectedValue = Value + Space * Err "value expected" 689 | ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() 690 | local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue) 691 | ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() 692 | local DecodeValue = ExpectedValue * g.Cp () 693 | 694 | function json.decode (str, pos, nullval, ...) 695 | local state = {} 696 | state.objectmeta, state.arraymeta = optionalmetatables(...) 697 | local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) 698 | if state.msg then 699 | return nil, state.pos, state.msg 700 | else 701 | return obj, retpos 702 | end 703 | end 704 | 705 | -- use this function only once: 706 | json.use_lpeg = function () return json end 707 | 708 | json.using_lpeg = true 709 | 710 | return json -- so you can get the module using json = require "dkjson".use_lpeg() 711 | end 712 | 713 | if always_try_using_lpeg then 714 | pcall (json.use_lpeg) 715 | end 716 | 717 | json.parse = json.decode 718 | json.stringify = json.encode 719 | 720 | -------------------------------------------------------------------------------- /deps/pretty-print/16.lua: -------------------------------------------------------------------------------- 1 | 2 | -- nice color theme using 16 ansi colors 3 | 4 | return { 5 | property = "0;37", -- white 6 | sep = "1;30", -- bright-black 7 | braces = "1;30", -- bright-black 8 | 9 | ["nil"] = "1;30", -- bright-black 10 | boolean = "0;33", -- yellow 11 | number = "1;33", -- bright-yellow 12 | string = "0;32", -- green 13 | quotes = "1;32", -- bright-green 14 | escape = "1;32", -- bright-green 15 | ["function"] = "0;35", -- purple 16 | thread = "1;35", -- bright-purple 17 | 18 | table = "1;34", -- bright blue 19 | userdata = "1;36", -- bright cyan 20 | cdata = "0;36", -- cyan 21 | 22 | err = "1;31", -- bright red 23 | success = "1;33;42", -- bright-yellow on green 24 | failure = "1;33;41", -- bright-yellow on red 25 | highlight = "1;36;44", -- bright-cyan on blue 26 | } 27 | -------------------------------------------------------------------------------- /deps/pretty-print/256.lua: -------------------------------------------------------------------------------- 1 | 2 | -- nice color theme using ansi 256-mode colors 3 | 4 | return { 5 | property = "38;5;253", 6 | braces = "38;5;247", 7 | sep = "38;5;240", 8 | 9 | ["nil"] = "38;5;244", 10 | boolean = "38;5;220", -- yellow-orange 11 | number = "38;5;202", -- orange 12 | string = "38;5;34", -- darker green 13 | quotes = "38;5;40", -- green 14 | escape = "38;5;46", -- bright green 15 | ["function"] = "38;5;129", -- purple 16 | thread = "38;5;199", -- pink 17 | 18 | table = "38;5;27", -- blue 19 | userdata = "38;5;39", -- blue2 20 | cdata = "38;5;69", -- teal 21 | 22 | err = "38;5;196", -- bright red 23 | success = "38;5;120;48;5;22", -- bright green on dark green 24 | failure = "38;5;215;48;5;52", -- bright red on dark red 25 | highlight = "38;5;45;48;5;236", -- bright teal on dark grey 26 | } 27 | -------------------------------------------------------------------------------- /deps/pretty-print/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2014-2015 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | --]] 18 | exports.name = "luvit/pretty-print" 19 | exports.version = "0.1.0" 20 | 21 | local uv = require('uv') 22 | local env = require('env') 23 | 24 | local prettyPrint, dump, strip, color, colorize, loadColors 25 | local theme = {} 26 | local useColors = false 27 | local defaultTheme 28 | 29 | local stdout, stdin, stderr, width 30 | 31 | local quote, quote2, dquote, dquote2, obracket, cbracket, obrace, cbrace, comma, equals, controls 32 | 33 | local themes = { 34 | [16] = require('./16.lua'), 35 | [256] = require('./256.lua'), 36 | } 37 | 38 | local special = { 39 | [7] = 'a', 40 | [8] = 'b', 41 | [9] = 't', 42 | [10] = 'n', 43 | [11] = 'v', 44 | [12] = 'f', 45 | [13] = 'r' 46 | } 47 | 48 | function strip(str) 49 | return string.gsub(str, '\027%[[^m]*m', '') 50 | end 51 | 52 | 53 | function loadColors(index) 54 | if index == nil then index = defaultTheme end 55 | 56 | -- Remove the old theme 57 | for key in pairs(theme) do 58 | theme[key] = nil 59 | end 60 | 61 | if index then 62 | local new = themes[index] 63 | if not new then error("Invalid theme index: " .. tostring(index)) end 64 | -- Add the new theme 65 | for key in pairs(new) do 66 | theme[key] = new[key] 67 | end 68 | useColors = true 69 | else 70 | useColors = false 71 | end 72 | 73 | quote = colorize('quotes', "'", 'string') 74 | quote2 = colorize('quotes', "'") 75 | dquote = colorize('quotes', '"', 'string') 76 | dquote2 = colorize('quotes', '"') 77 | obrace = colorize('braces', '{ ') 78 | cbrace = colorize('braces', '}') 79 | obracket = colorize('property', '[') 80 | cbracket = colorize('property', ']') 81 | comma = colorize('sep', ', ') 82 | equals = colorize('sep', ' = ') 83 | 84 | controls = {} 85 | for i = 0, 31 do 86 | local c = special[i] 87 | if not c then 88 | if i < 10 then 89 | c = "00" .. tostring(i) 90 | else 91 | c = "0" .. tostring(i) 92 | end 93 | end 94 | controls[i] = colorize('escape', '\\' .. c, 'string') 95 | end 96 | controls[92] = colorize('escape', '\\\\', 'string') 97 | controls[34] = colorize('escape', '\\"', 'string') 98 | controls[39] = colorize('escape', "\\'", 'string') 99 | for i = 128, 255 do 100 | local c 101 | if i < 100 then 102 | c = "0" .. tostring(i) 103 | else 104 | c = tostring(i) 105 | end 106 | controls[i] = colorize('escape', '\\' .. c, 'string') 107 | end 108 | 109 | end 110 | 111 | function color(colorName) 112 | return '\27[' .. (theme[colorName] or '0') .. 'm' 113 | end 114 | 115 | function colorize(colorName, string, resetName) 116 | return useColors and 117 | (color(colorName) .. tostring(string) .. color(resetName)) or 118 | tostring(string) 119 | end 120 | 121 | local function stringEscape(c) 122 | return controls[string.byte(c, 1)] 123 | end 124 | 125 | function dump(value) 126 | local seen = {} 127 | local output = {} 128 | local offset = 0 129 | local stack = {} 130 | 131 | local function recalcOffset(index) 132 | for i = index + 1, #output do 133 | local m = string.match(output[i], "\n([^\n]*)$") 134 | if m then 135 | offset = #(strip(m)) 136 | else 137 | offset = offset + #(strip(output[i])) 138 | end 139 | end 140 | end 141 | 142 | local function write(text, length) 143 | if not length then length = #(strip(text)) end 144 | -- Create room for data by opening parent blocks 145 | -- Start at the root and go down. 146 | local i = 1 147 | while offset + length > width and stack[i] do 148 | local entry = stack[i] 149 | if not entry.opened then 150 | entry.opened = true 151 | table.insert(output, entry.index + 1, "\n" .. string.rep(" ", i)) 152 | -- Recalculate the offset 153 | recalcOffset(entry.index) 154 | -- Bump the index of all deeper entries 155 | for j = i + 1, #stack do 156 | stack[j].index = stack[j].index + 1 157 | end 158 | end 159 | i = i + 1 160 | end 161 | output[#output + 1] = text 162 | offset = offset + length 163 | if offset > width then 164 | dump(stack) 165 | end 166 | end 167 | 168 | local function indent() 169 | stack[#stack + 1] = { 170 | index = #output, 171 | opened = false, 172 | } 173 | end 174 | 175 | local function unindent() 176 | stack[#stack] = nil 177 | end 178 | 179 | local function process(value) 180 | local typ = type(value) 181 | if typ == 'string' then 182 | if string.match(value, "'") and not string.match(value, '"') then 183 | write(dquote .. string.gsub(value, '[%c\\\128-\255]', stringEscape) .. dquote2) 184 | else 185 | write(quote .. string.gsub(value, "[%c\\'\128-\255]", stringEscape) .. quote2) 186 | end 187 | elseif typ == 'table' and not seen[value] then 188 | 189 | seen[value] = true 190 | write(obrace) 191 | local i = 1 192 | -- Count the number of keys so we know when to stop adding commas 193 | local total = 0 194 | for _ in pairs(value) do total = total + 1 end 195 | 196 | for k, v in pairs(value) do 197 | indent() 198 | if k == i then 199 | -- if the key matches the index, don't show it. 200 | -- This is how lists print without keys 201 | process(v) 202 | else 203 | if type(k) == "string" and string.find(k,"^[%a_][%a%d_]*$") then 204 | write(colorize("property", k) .. equals) 205 | else 206 | write(obracket) 207 | process(k) 208 | write(cbracket .. equals) 209 | end 210 | if type(v) == "table" then 211 | process(v) 212 | else 213 | indent() 214 | process(v) 215 | unindent() 216 | end 217 | end 218 | if i < total then 219 | write(comma) 220 | else 221 | write(" ") 222 | end 223 | i = i + 1 224 | unindent() 225 | end 226 | write(cbrace) 227 | else 228 | write(colorize(typ, tostring(value))) 229 | end 230 | end 231 | 232 | process(value) 233 | 234 | return table.concat(output, "") 235 | end 236 | 237 | -- Print replacement that goes through libuv. This is useful on windows 238 | -- to use libuv's code to translate ansi escape codes to windows API calls. 239 | function _G.print(...) 240 | local n = select('#', ...) 241 | local arguments = {...} 242 | for i = 1, n do 243 | arguments[i] = tostring(arguments[i]) 244 | end 245 | uv.write(stdout, table.concat(arguments, "\t") .. "\n") 246 | end 247 | 248 | function prettyPrint(...) 249 | local n = select('#', ...) 250 | local arguments = { ... } 251 | 252 | for i = 1, n do 253 | arguments[i] = dump(arguments[i]) 254 | end 255 | 256 | print(table.concat(arguments, "\t")) 257 | end 258 | 259 | function strip(str) 260 | return string.gsub(str, '\027%[[^m]*m', '') 261 | end 262 | 263 | if uv.guess_handle(0) == 'tty' then 264 | stdin = assert(uv.new_tty(0, true)) 265 | else 266 | stdin = uv.new_pipe(false) 267 | uv.pipe_open(stdin, 0) 268 | end 269 | 270 | if uv.guess_handle(1) == 'tty' then 271 | stdout = assert(uv.new_tty(1, false)) 272 | width = uv.tty_get_winsize(stdout) 273 | -- auto-detect when 16 color mode should be used 274 | local term = env.get("TERM") 275 | if term == 'xterm' or term == 'xterm-256color' then 276 | defaultTheme = 256 277 | else 278 | defaultTheme = 16 279 | end 280 | else 281 | stdout = uv.new_pipe(false) 282 | uv.pipe_open(stdout, 1) 283 | width = 80 284 | end 285 | loadColors() 286 | 287 | if uv.guess_handle(2) == 'tty' then 288 | stderr = assert(uv.new_tty(2, false)) 289 | else 290 | stderr = uv.new_pipe(false) 291 | uv.pipe_open(stderr, 2) 292 | end 293 | 294 | exports.loadColors = loadColors 295 | exports.theme = theme 296 | exports.print = print 297 | exports.prettyPrint = prettyPrint 298 | exports.dump = dump 299 | exports.color = color 300 | exports.colorize = colorize 301 | exports.stdin = stdin 302 | exports.stdout = stdout 303 | exports.stderr = stderr 304 | exports.strip = strip 305 | -------------------------------------------------------------------------------- /deps/require.lua: -------------------------------------------------------------------------------- 1 | 2 | if exports then 3 | exports.name = "luvit/require" 4 | exports.version = "0.2.1" 5 | end 6 | 7 | local luvi = require('luvi') 8 | local bundle = luvi.bundle 9 | local pathJoin = luvi.path.join 10 | local env = require('env') 11 | local os = require('ffi').os 12 | local uv = require('uv') 13 | 14 | local realRequire = _G.require 15 | 16 | local tmpBase = os == "Windows" and (env.get("TMP") or uv.cwd()) or 17 | (env.get("TMPDIR") or '/tmp') 18 | local binExt = os == "Windows" and ".dll" or ".so" 19 | 20 | -- Package sources 21 | -- $author/$name@$version -> resolves to hash, cached in memory 22 | -- bundle:full/bundle/path 23 | -- full/unix/path 24 | -- C:\\full\windows\path 25 | 26 | local fileCache = {} 27 | local function readFile(path) 28 | assert(path) 29 | local data = fileCache[path] 30 | if data ~= nil then return data end 31 | local prefix = path:match("^bundle:/*") 32 | if prefix then 33 | data = bundle.readfile(path:sub(#prefix + 1)) 34 | else 35 | local stat = uv.fs_stat(path) 36 | if stat and stat.type == "file" then 37 | local fd = uv.fs_open(path, "r", 511) 38 | if fd then 39 | data = uv.fs_read(fd, stat.size, -1) 40 | uv.fs_close(fd) 41 | end 42 | end 43 | end 44 | fileCache[path] = data and true or false 45 | return data 46 | end 47 | 48 | local dirCache = {} 49 | local function isDir(path) 50 | assert(path) 51 | local is = dirCache[path] 52 | if is ~= nil then return is end 53 | local prefix = path:match("^bundle:/*") 54 | local stat 55 | if prefix then 56 | stat = bundle.stat(path:sub(#prefix + 1)) 57 | else 58 | stat = uv.fs_stat(path) 59 | end 60 | is = stat and (stat.type == "directory") or false 61 | dirCache[path] = is 62 | return is 63 | end 64 | 65 | 66 | local types = { ".lua", binExt } 67 | 68 | local function fixedRequire(path) 69 | assert(path) 70 | local fullPath = path 71 | local data = readFile(fullPath) 72 | if not data then 73 | for i = 1, #types do 74 | fullPath = path .. types[i] 75 | data = readFile(fullPath) 76 | if data then break end 77 | fullPath = pathJoin(path, "init" .. types[i]) 78 | data = readFile(fullPath) 79 | if data then break end 80 | end 81 | if not data then return end 82 | end 83 | local prefix = fullPath:match("^bundle:") 84 | local normalizedPath = fullPath 85 | if prefix == "bundle:" and bundle.base then 86 | normalizedPath = fullPath:gsub(prefix, bundle.base) 87 | end 88 | 89 | return data, fullPath, normalizedPath 90 | end 91 | 92 | 93 | local skips = {} 94 | local function moduleRequire(base, name) 95 | assert(base and name) 96 | while true do 97 | if not skips[base] then 98 | local mod, path, key 99 | if isDir(pathJoin(base, "libs")) then 100 | mod, path, key = fixedRequire(pathJoin(base, "libs", name)) 101 | if mod then return mod, path, key end 102 | end 103 | if isDir(pathJoin(base, "deps")) then 104 | mod, path, key = fixedRequire(pathJoin(base, "deps", name)) 105 | if mod then return mod, path, key end 106 | end 107 | end 108 | 109 | -- Stop at filesystem or prefix root (58 is ":") 110 | if base == "/" or base:byte(-1) == 58 then break end 111 | base = pathJoin(base, "..") 112 | end 113 | -- If we didn't find it outside the bundle, look inside the bundle. 114 | if not base:match("^bundle:/*") then 115 | return moduleRequire("bundle:", name) 116 | end 117 | end 118 | 119 | local moduleCache = {} 120 | 121 | local function generator(modulePath) 122 | assert(modulePath, "Missing path to require generator") 123 | 124 | -- Convert windows paths to unix paths (mostly) 125 | local path = modulePath:gsub("\\", "/") 126 | -- Normalize slashes around prefix to be exactly one after 127 | path = path:gsub("^/*([^/:]+:)/*", "%1/") 128 | 129 | local base = pathJoin(path, "..") 130 | 131 | local function resolve(name) 132 | assert(name, "Missing name to resolve") 133 | if name:byte(1) == 46 then -- Starts with "." 134 | return fixedRequire(pathJoin(base, name)) 135 | elseif name:byte(1) == 47 then -- Starts with "/" 136 | return fixedRequire(name) 137 | end 138 | return moduleRequire(base, name) 139 | end 140 | 141 | local function require(name) 142 | assert(name, "Missing name to require") 143 | 144 | if package.preload[name] or package.loaded[name] then 145 | return realRequire(name) 146 | end 147 | 148 | -- Resolve the path 149 | local data, path, key = resolve(name) 150 | if not path then 151 | local success, value = pcall(realRequire, name) 152 | if success then return value end 153 | if not success then 154 | error("No such module '" .. name .. "' in '" .. modulePath .. "'") 155 | end 156 | end 157 | 158 | -- Check in the cache for this module 159 | local module = moduleCache[key] 160 | if module then return module.exports end 161 | -- Put a new module in the cache if not 162 | module = { path = path, dir = pathJoin(path, ".."), exports = {} } 163 | moduleCache[key] = module 164 | 165 | local ext = path:match("%.[^/]+$") 166 | if ext == ".lua" then 167 | local fn = assert(loadstring(data, path)) 168 | local global = { 169 | module = module, 170 | exports = module.exports 171 | } 172 | global.require, module.resolve = generator(path) 173 | setfenv(fn, setmetatable(global, { __index = _G })) 174 | local ret = fn() 175 | 176 | -- Allow returning the exports as well 177 | if ret then module.exports = ret end 178 | 179 | elseif ext == binExt then 180 | local fnName = "luaopen_" .. name:match("[^/]+$"):match("^[^%.]+") 181 | local fn 182 | if uv.fs_access(path, "r") then 183 | -- If it's a real file, load it directly 184 | fn = assert(package.loadlib(path, fnName)) 185 | else 186 | -- Otherwise, copy to a temporary folder and read from there 187 | local dir = assert(uv.fs_mkdtemp(pathJoin(tmpBase, "lib-XXXXXX"))) 188 | local fd = uv.fs_open(path, "w", 384) -- 0600 189 | uv.fs_write(fd, data, 0) 190 | uv.fs_close(fd) 191 | fn = assert(package.loadlib(path, fnName)) 192 | uv.fs_unlink(path) 193 | uv.fs_rmdir(dir) 194 | end 195 | module.exports = fn() 196 | else 197 | error("Unknown type at '" .. path .. "' for '" .. name .. "' in '" .. modulePath .. "'") 198 | end 199 | return module.exports 200 | end 201 | 202 | return require, resolve, moduleCache 203 | end 204 | 205 | return generator 206 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rye-Demo 5 | 6 | 7 |

Welcome %USER_AGENT%

8 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /libs/auto-headers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Response automatic values: 4 | - Auto Server header 5 | - Auto Date Header 6 | - code defaults to 404 with body "Not Found\n" 7 | - if there is a string body add Content-Length and ETag if missing 8 | - if string body and no Content-Type, use text/plain for valid utf-8, application/octet-stream otherwise 9 | - Auto add "; charset=utf-8" to Content-Type when body is known to be valid utf-8 10 | - Auto 304 responses for if-none-match requests 11 | - Auto strip body with HEAD requests 12 | - Auto chunked encoding if body with unknown length 13 | - if Connection header set and not keep-alive, set res.keepAlive to false 14 | - Add Connection Keep-Alive/Close if not found based on res.keepAlive 15 | 16 | --TODO: utf8 scanning 17 | 18 | ]] 19 | 20 | local digest = require('openssl').digest.digest 21 | local date = require('os').date 22 | 23 | local serverName = "creationix/web-app v" .. require('../package').version 24 | 25 | 26 | return function (req, res, go) 27 | local isHead = false 28 | if req.method == "HEAD" then 29 | req.method = "GET" 30 | isHead = true 31 | end 32 | 33 | local requested = req.headers["if-none-match"] 34 | 35 | go() 36 | 37 | -- We could use the fancy metatable, but this is much faster 38 | local lowerHeaders = {} 39 | local headers = res.headers 40 | for i = 1, #headers do 41 | local key, value = unpack(headers[i]) 42 | lowerHeaders[key:lower()] = value 43 | end 44 | 45 | 46 | if not lowerHeaders.server then 47 | headers[#headers + 1] = {"Server", serverName} 48 | end 49 | if not lowerHeaders.date then 50 | headers[#headers + 1] = {"Date", date("!%a, %d %b %Y %H:%M:%S GMT")} 51 | end 52 | 53 | if not lowerHeaders.connection then 54 | if req.keepAlive then 55 | lowerHeaders.connection = "Keep-Alive" 56 | headers[#headers + 1] = {"Connection", "Keep-Alive"} 57 | else 58 | headers[#headers + 1] = {"Connection", "Close"} 59 | end 60 | end 61 | res.keepAlive = lowerHeaders.connection:lower() == "keep-alive" 62 | 63 | local body = res.body 64 | if body then 65 | local needLength = not lowerHeaders["content-length"] and not lowerHeaders["transfer-encoding"] 66 | if type(body) == "string" then 67 | if needLength then 68 | headers[#headers + 1] = {"Content-Length", #body} 69 | end 70 | if not lowerHeaders.etag then 71 | local etag = '"' .. digest("sha1", body) .. '"' 72 | lowerHeaders.etag = etag 73 | headers[#headers + 1] = {"ETag", etag} 74 | end 75 | else 76 | if needLength then 77 | headers[#headers + 1] = {"Transfer-Encoding", "chunked"} 78 | end 79 | end 80 | if not lowerHeaders["content-type"] then 81 | headers[#headers + 1] = {"Content-Type", "text/plain"} 82 | end 83 | end 84 | 85 | local etag = lowerHeaders.etag 86 | if requested and res.code >= 200 and res.code < 300 and requested == etag then 87 | res.code = 304 88 | body = nil 89 | end 90 | 91 | if isHead then body = nil end 92 | res.body = body 93 | end 94 | -------------------------------------------------------------------------------- /libs/etag-cache.lua: -------------------------------------------------------------------------------- 1 | local function clone(headers) 2 | local copy = setmetatable({}, getmetatable(headers)) 3 | for i = 1, #headers do 4 | copy[i] = headers[i] 5 | end 6 | return copy 7 | end 8 | 9 | local cache = {} 10 | return function (req, res, go) 11 | local requested = req.headers["If-None-Match"] 12 | local host = req.headers.Host 13 | local key = host and host .. "|" .. req.path or req.path 14 | local cached = cache[key] 15 | if not requested and cached then 16 | req.headers["If-None-Match"] = cached.etag 17 | end 18 | go() 19 | local etag = res.headers.ETag 20 | if not etag then return end 21 | if res.code >= 200 and res.code < 300 then 22 | local body = res.body 23 | if not body or type(body) == "string" then 24 | cache[key] = { 25 | etag = etag, 26 | code = res.code, 27 | headers = clone(res.headers), 28 | body = body 29 | } 30 | end 31 | elseif res.code == 304 then 32 | if not requested and cached and etag == cached.etag then 33 | res.code = cached.code 34 | res.headers = clone(cached.headers) 35 | res.body = cached.body 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /libs/git-fs.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/git-fs" 2 | exports.version = "0.1.0" 3 | exports.dependencies = { 4 | "creationix/git@0.1.1", 5 | "creationix/hex-bin@1.0.0", 6 | } 7 | 8 | --[[ 9 | 10 | Git Object Database 11 | =================== 12 | 13 | Consumes a storage interface and return a git database interface 14 | 15 | db.has(hash) -> bool - check if db has an object 16 | db.load(hash) -> raw - load raw data, nil if not found 17 | db.loadAny(hash) -> kind, value - pre-decode data, error if not found 18 | db.loadAs(kind, hash) -> value - pre-decode and check type or error 19 | db.save(raw) -> hash - save pre-encoded and framed data 20 | db.saveAs(kind, value) -> hash - encode, frame and save to objects/$ha/$sh 21 | db.hashes() -> iter - Iterate over all hashes 22 | 23 | db.getHead() -> hash - Read the hash via HEAD 24 | db.getRef(ref) -> hash - Read hash of a ref 25 | db.resolve(ref) -> hash - Given a hash, tag, branch, or HEAD, return the hash 26 | ]] 27 | 28 | local git = require('git') 29 | local miniz = require('miniz') 30 | local openssl = require('openssl') 31 | local hexBin = require('hex-bin') 32 | local uv = require('uv') 33 | 34 | local numToType = { 35 | [1] = "commit", 36 | [2] = "tree", 37 | [3] = "blob", 38 | [4] = "tag", 39 | [6] = "ofs-delta", 40 | [7] = "ref-delta", 41 | } 42 | 43 | local function applyDelta(base, delta) --> raw 44 | local deltaOffset = 0; 45 | 46 | -- Read a variable length number our of delta and move the offset. 47 | local function readLength() 48 | deltaOffset = deltaOffset + 1 49 | local byte = delta:byte(deltaOffset) 50 | local length = bit.band(byte, 0x7f) 51 | local shift = 7 52 | while bit.band(byte, 0x80) > 0 do 53 | deltaOffset = deltaOffset + 1 54 | byte = delta:byte(deltaOffset) 55 | length = bit.bor(length, bit.lshift(bit.band(byte, 0x7f), shift)) 56 | shift = shift + 7 57 | end 58 | return length 59 | end 60 | 61 | assert(#base == readLength(), "base length mismatch") 62 | 63 | local outLength = readLength() 64 | local parts = {} 65 | while deltaOffset < #delta do 66 | deltaOffset = deltaOffset + 1 67 | local byte = delta:byte(deltaOffset) 68 | 69 | if bit.band(byte, 0x80) > 0 then 70 | -- Copy command. Tells us offset in base and length to copy. 71 | local offset = 0 72 | local length = 0 73 | if bit.band(byte, 0x01) > 0 then 74 | deltaOffset = deltaOffset + 1 75 | offset = bit.bor(offset, delta:byte(deltaOffset)) 76 | end 77 | if bit.band(byte, 0x02) > 0 then 78 | deltaOffset = deltaOffset + 1 79 | offset = bit.bor(offset, bit.lshift(delta:byte(deltaOffset), 8)) 80 | end 81 | if bit.band(byte, 0x04) > 0 then 82 | deltaOffset = deltaOffset + 1 83 | offset = bit.bor(offset, bit.lshift(delta:byte(deltaOffset), 16)) 84 | end 85 | if bit.band(byte, 0x08) > 0 then 86 | deltaOffset = deltaOffset + 1 87 | offset = bit.bor(offset, bit.lshift(delta:byte(deltaOffset), 24)) 88 | end 89 | if bit.band(byte, 0x10) > 0 then 90 | deltaOffset = deltaOffset + 1 91 | length = bit.bor(length, delta:byte(deltaOffset)) 92 | end 93 | if bit.band(byte, 0x20) > 0 then 94 | deltaOffset = deltaOffset + 1 95 | length = bit.bor(length, bit.lshift(delta:byte(deltaOffset), 8)) 96 | end 97 | if bit.band(byte, 0x40) > 0 then 98 | deltaOffset = deltaOffset + 1 99 | length = bit.bor(length, bit.lshift(delta:byte(deltaOffset), 16)) 100 | end 101 | if length == 0 then length = 0x10000 end 102 | -- copy the data 103 | parts[#parts + 1] = base:sub(offset + 1, offset + length) 104 | elseif byte > 0 then 105 | -- Insert command, opcode byte is length itself 106 | parts[#parts + 1] = delta:sub(deltaOffset + 1, deltaOffset + byte) 107 | deltaOffset = deltaOffset + byte 108 | else 109 | error("Invalid opcode in delta") 110 | end 111 | end 112 | local out = table.concat(parts) 113 | assert(#out == outLength, "final size mismatch in delta application") 114 | return table.concat(parts) 115 | end 116 | 117 | local function readUint32(buffer, offset) 118 | offset = offset or 0 119 | assert(#buffer >= offset + 4, "not enough buffer") 120 | return bit.bor( 121 | bit.lshift(buffer:byte(offset + 1), 24), 122 | bit.lshift(buffer:byte(offset + 2), 16), 123 | bit.lshift(buffer:byte(offset + 3), 8), 124 | buffer:byte(offset + 4) 125 | ) 126 | end 127 | 128 | local function readUint64(buffer, offset) 129 | offset = offset or 0 130 | assert(#buffer >= offset + 8, "not enough buffer") 131 | return 132 | (bit.lshift(buffer:byte(offset + 1), 24) + 133 | bit.lshift(buffer:byte(offset + 2), 16) + 134 | bit.lshift(buffer:byte(offset + 3), 8) + 135 | buffer:byte(offset + 4)) * 0x100000000 + 136 | bit.lshift(buffer:byte(offset + 5), 24) + 137 | bit.lshift(buffer:byte(offset + 6), 16) + 138 | bit.lshift(buffer:byte(offset + 7), 8) + 139 | buffer:byte(offset + 8) 140 | end 141 | 142 | local function assertHash(hash) 143 | assert(hash and #hash == 40 and hash:match("^%x+$"), "Invalid hash") 144 | end 145 | 146 | local function hashPath(hash) 147 | return string.format("objects/%s/%s", hash:sub(1, 2), hash:sub(3)) 148 | end 149 | 150 | return function (storage) 151 | 152 | local encoders = git.encoders 153 | local decoders = git.decoders 154 | local frame = git.frame 155 | local deframe = git.deframe 156 | local deflate = miniz.deflate 157 | local inflate = miniz.inflate 158 | local digest = openssl.digest.digest 159 | local binToHex = hexBin.binToHex 160 | local hexToBin = hexBin.hexToBin 161 | 162 | local db = { storage = storage } 163 | local fs = storage.fs 164 | 165 | -- Initialize the git file storage tree if it does't exist yet 166 | if not fs.access("HEAD") then 167 | assert(fs.mkdirp("objects")) 168 | assert(fs.mkdirp("refs/tags")) 169 | assert(fs.writeFile("HEAD", "ref: refs/heads/master\n")) 170 | assert(fs.writeFile("config", [[ 171 | [core] 172 | repositoryformatversion = 0 173 | filemode = true 174 | bare = true 175 | [gc] 176 | auto = 0 177 | ]])) 178 | end 179 | 180 | local packs = {} 181 | local function makePack(packHash) 182 | local pack = packs[packHash] 183 | if pack then 184 | if pack.waiting then 185 | pack.waiting[#pack.waiting + 1] = coroutine.running() 186 | return coroutine.yield() 187 | end 188 | return pack 189 | end 190 | local waiting = {} 191 | pack = { waiting=waiting } 192 | 193 | local timer, indexFd, packFd, indexLength 194 | local hashOffset, crcOffset 195 | local offsets, lengths, packSize 196 | 197 | local function close() 198 | if pack then 199 | pack.waiting = nil 200 | if packs[packHash] == pack then 201 | packs[packHash] = nil 202 | end 203 | pack = nil 204 | end 205 | if timer then 206 | timer:stop() 207 | timer:close() 208 | timer = nil 209 | end 210 | if indexFd then 211 | fs.close(indexFd) 212 | indexFd = nil 213 | end 214 | if packFd then 215 | fs.close(packFd) 216 | packFd = nil 217 | end 218 | end 219 | 220 | local function timeout() 221 | coroutine.wrap(close)() 222 | end 223 | 224 | 225 | timer = uv.new_timer() 226 | uv.unref(timer) 227 | timer:start(2000, 2000, timeout) 228 | 229 | packFd = assert(fs.open("objects/pack/pack-" .. packHash .. ".pack")) 230 | local stat = assert(fs.fstat(packFd)) 231 | packSize = stat.size 232 | assert(fs.read(packFd, 8, 0) == "PACK\0\0\0\2", "Only v2 pack files supported") 233 | 234 | indexFd = assert(fs.open("objects/pack/pack-" .. packHash .. ".idx")) 235 | assert(fs.read(indexFd, 8, 0) == '\255tOc\0\0\0\2', 'Only pack index v2 supported') 236 | indexLength = readUint32(assert(fs.read(indexFd, 4, 8 + 255 * 4))) 237 | hashOffset = 8 + 255 * 4 + 4 238 | crcOffset = hashOffset + 20 * indexLength 239 | local lengthOffset = crcOffset + 4 * indexLength 240 | local largeOffset = lengthOffset + 4 * indexLength 241 | offsets = {} 242 | lengths = {} 243 | local sorted = {} 244 | local data = assert(fs.read(indexFd, 4 * indexLength, lengthOffset)) 245 | for i = 1, indexLength do 246 | local offset = readUint32(data, (i - 1) * 4) 247 | if bit.band(offset, 0x80000000) > 0 then 248 | error("TODO: Implement large offsets properly") 249 | offset = largeOffset + bit.band(offset, 0x7fffffff) * 8; 250 | offset = readUint64(assert(fs.read(indexFd, 8, offset))) 251 | end 252 | offsets[i] = offset 253 | sorted[i] = offset 254 | end 255 | table.sort(sorted) 256 | for i = 1, indexLength do 257 | local offset = offsets[i] 258 | local length 259 | for j = 1, indexLength - 1 do 260 | if sorted[j] == offset then 261 | length = sorted[j + 1] - offset 262 | break 263 | end 264 | end 265 | lengths[i] = length or (packSize - offset - 20) 266 | end 267 | 268 | local function loadHash(hash) --> offset 269 | 270 | -- Read first fan-out table to get index into offset table 271 | local prefix = hexToBin(hash:sub(1, 2)):byte(1) 272 | local first = prefix == 0 and 0 or readUint32(assert(fs.read(indexFd, 4, 8 + (prefix - 1) * 4))) 273 | local last = readUint32(assert(fs.read(indexFd, 4, 8 + prefix * 4))) 274 | 275 | for index = first, last do 276 | local start = hashOffset + index * 20 277 | local foundHash = binToHex(assert(fs.read(indexFd, 20, start))) 278 | if foundHash == hash then 279 | index = index + 1 280 | return offsets[index], lengths[index] 281 | end 282 | end 283 | end 284 | 285 | local function loadRaw(offset, length) -->raw 286 | -- Shouldn't need more than 32 bytes to read variable length header and 287 | -- optional hash or offset 288 | local chunk = assert(fs.read(packFd, 32, offset)) 289 | local byte = chunk:byte(1) 290 | 291 | -- Parse out the git type 292 | local kind = numToType[bit.band(bit.rshift(byte, 4), 0x7)] 293 | 294 | -- Parse out the uncompressed length 295 | local size = bit.band(byte, 0xf) 296 | local left = 4 297 | local i = 2 298 | while bit.band(byte, 0x80) > 0 do 299 | byte = chunk:byte(i) 300 | i = i + 1 301 | size = bit.bor(size, bit.lshift(bit.band(byte, 0x7f), left)) 302 | left = left + 7 303 | end 304 | 305 | -- Optionally parse out the hash or offset for deltas 306 | local ref 307 | if kind == "ref-delta" then 308 | ref = binToHex(chunk:sub(i + 1, i + 20)) 309 | i = i + 20 310 | elseif kind == "ofs-delta" then 311 | local byte = chunk:byte(i) 312 | i = i + 1 313 | ref = bit.band(byte, 0x7f) 314 | while bit.band(byte, 0x80) > 0 do 315 | byte = chunk:byte(i) 316 | i = i + 1 317 | ref = bit.bor(bit.lshift(ref + 1, 7), bit.band(byte, 0x7f)) 318 | end 319 | end 320 | 321 | local compressed = assert(fs.read(packFd, length, offset + i - 1)) 322 | local raw = inflate(compressed, 1) 323 | 324 | assert(#raw == size, "inflate error or size mismatch at offset " .. offset) 325 | 326 | if kind == "ref-delta" then 327 | error("TODO: handle ref-delta") 328 | elseif kind == "ofs-delta" then 329 | local base 330 | kind, base = loadRaw(offset - ref) 331 | raw = applyDelta(base, raw) 332 | end 333 | return kind, raw 334 | end 335 | 336 | function pack.load(hash) --> raw 337 | if not pack then 338 | return makePack(packHash).load(hash) 339 | end 340 | timer:again() 341 | local success, result = pcall(function () 342 | local offset, length = loadHash(hash) 343 | if not offset then return end 344 | local kind, raw = loadRaw(offset, length) 345 | return frame(kind, raw) 346 | end) 347 | if success then return result end 348 | -- close() 349 | error(result) 350 | end 351 | 352 | packs[packHash] = pack 353 | pack.waiting = nil 354 | for i = 1, #waiting do 355 | assert(coroutine.resume(waiting[i], pack)) 356 | end 357 | 358 | return pack 359 | end 360 | 361 | function db.has(hash) 362 | assertHash(hash) 363 | return storage.read(hashPath(hash)) and true or false 364 | end 365 | 366 | function db.load(hash) 367 | hash = db.resolve(hash) 368 | assertHash(hash) 369 | local compressed, err = storage.read(hashPath(hash)) 370 | if not compressed then 371 | for file in storage.leaves("objects/pack") do 372 | local packHash = file:match("^pack%-(%x+)%.idx$") 373 | if packHash then 374 | local raw 375 | raw, err = makePack(packHash).load(hash) 376 | if raw then return raw end 377 | end 378 | end 379 | return nil, err 380 | end 381 | return inflate(compressed, 1) 382 | end 383 | 384 | function db.loadAny(hash) 385 | local raw = assert(db.load(hash), "no such hash") 386 | local kind, value = deframe(raw) 387 | return kind, decoders[kind](value) 388 | end 389 | 390 | function db.loadAs(kind, hash) 391 | local actualKind, value = db.loadAny(hash) 392 | assert(kind == actualKind, "Kind mismatch") 393 | return value 394 | end 395 | 396 | function db.save(raw) 397 | local hash = digest("sha1", raw) 398 | -- 0x1000 = TDEFL_WRITE_ZLIB_HEADER 399 | -- 4095 = Huffman+LZ (slowest/best compression) 400 | storage.put(hashPath(hash), deflate(raw, 0x1000 + 4095)) 401 | return hash 402 | end 403 | 404 | function db.saveAs(kind, value) 405 | if type(value) ~= "string" then 406 | value = encoders[kind](value) 407 | end 408 | return db.save(frame(kind, value)) 409 | end 410 | 411 | function db.hashes() 412 | local groups = storage.nodes("objects") 413 | local prefix, iter 414 | return function () 415 | while true do 416 | if prefix then 417 | local rest = iter() 418 | if rest then return prefix .. rest end 419 | prefix = nil 420 | iter = nil 421 | end 422 | prefix = groups() 423 | if not prefix then return end 424 | iter = storage.leaves("objects/" .. prefix) 425 | end 426 | end 427 | end 428 | 429 | function db.getHead() 430 | local head = storage.read("HEAD") 431 | if not head then return end 432 | local ref = head:match("^ref: *([^\n]+)") 433 | return ref and db.getRef(ref) 434 | end 435 | 436 | function db.getRef(ref) 437 | local hash = storage.read(ref) 438 | if hash then return hash:match("%x+") end 439 | local refs = storage.read("packed-refs") 440 | return refs and refs:match("(%x+) " .. ref) 441 | end 442 | 443 | function db.resolve(ref) 444 | if ref == "HEAD" then return db.getHead() end 445 | local hash = ref:match("^%x+$") 446 | if hash and #hash == 40 then return hash end 447 | return db.getRef(ref) 448 | or db.getRef("refs/heads/" .. ref) 449 | or db.getRef("refs/tags/" .. ref) 450 | end 451 | 452 | return db 453 | end 454 | -------------------------------------------------------------------------------- /libs/git-hash-cache.lua: -------------------------------------------------------------------------------- 1 | return function (db, limit, max) 2 | local hashCache = {} 3 | local count = 0 4 | local realLoad = db.load 5 | function db.load(hash) 6 | local raw = hashCache[hash] 7 | if raw then return raw end 8 | raw = realLoad(hash) 9 | local size = #raw 10 | if size < limit then 11 | count = count + size 12 | if count > max then 13 | print("reset hash cache") 14 | hashCache = {} 15 | count = size 16 | end 17 | hashCache[hash] = raw 18 | end 19 | return raw 20 | end 21 | return db 22 | end 23 | -------------------------------------------------------------------------------- /libs/git-ref-cache.lua: -------------------------------------------------------------------------------- 1 | local uv = require('uv') 2 | 3 | return function (db, timeout) 4 | local refCache = {} 5 | 6 | local headCache 7 | local headTime 8 | 9 | local realGetHead = db.getHead 10 | function db.getHead() 11 | local now = uv.now() 12 | if headCache and now < headTime + timeout then 13 | return headCache 14 | end 15 | headCache = realGetHead() 16 | headTime = now 17 | return headCache 18 | end 19 | local realGetRef = db.getRef 20 | function db.getRef(ref) 21 | local cached = refCache[ref] 22 | local now = uv.now() 23 | if cached and now < cached.time + timeout then 24 | return cached.hash 25 | end 26 | local hash = realGetRef(ref) 27 | refCache[ref] = { 28 | time = now, 29 | hash = hash 30 | } 31 | return hash 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /libs/git-serve.lua: -------------------------------------------------------------------------------- 1 | local digest = require('openssl').digest.digest 2 | local git = require('git') 3 | local modes = git.modes 4 | local listToMap = git.listToMap 5 | local JSON = require('json') 6 | local getType = require('mime').getType 7 | 8 | 9 | return function (db, ref) 10 | return function (req, res, go) 11 | local commitHash = db.resolve(ref) 12 | local path = req.params.path or req.path 13 | local fullPath = commitHash .. ":" .. path 14 | local etag = '"dir-' .. digest("sha1", fullPath) .. '"' 15 | if etag == req.headers["If-None-Match"] then 16 | res.code = 304 17 | res.headers.ETag = etag 18 | return 19 | end 20 | local hash = db.loadAs("commit", commitHash).tree 21 | for part in path:gmatch("[^/]+") do 22 | local tree = listToMap(db.loadAs("tree", hash)) 23 | local entry = tree[part] 24 | if entry.mode == modes.commit then 25 | error("Submodules not implemented yet") 26 | end 27 | if not entry then return go() end 28 | hash = entry.hash 29 | end 30 | if not hash then return go() end 31 | 32 | local function render(kind, value) 33 | if kind == "tree" then 34 | if req.path:sub(-1) ~= "/" then 35 | res.code = 301 36 | res.headers.Location = req.path .. "/" 37 | return 38 | end 39 | for i = 1, #value do 40 | local entry = value[i] 41 | if entry.name == "index.html" and modes.isFile(entry.mode) then 42 | path = path .. "index.html" 43 | return render(db.loadAny(entry.hash)) 44 | end 45 | entry.type = modes.toType(entry.mode) 46 | if entry.mode == modes.tree or modes.isBlob(entry.mode) then 47 | local url = "http://" .. req.headers.Host .. req.path .. entry.name 48 | if entry.mode == modes.tree then url = url .. "/" end 49 | value[i].url = url 50 | end 51 | end 52 | res.code = 200 53 | res.headers["Content-Type"] = "application/json" 54 | res.headers.ETag = etag 55 | res.body = JSON.stringify(value) .. "\n" 56 | return 57 | elseif kind == "blob" then 58 | res.code = 200 59 | res.headers["Content-Type"] = getType(path) 60 | res.headers.ETag = etag 61 | res.body = value 62 | return 63 | else 64 | error("Unsupported kind: " .. kind) 65 | end 66 | end 67 | return render(db.loadAny(hash)) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /libs/logger.lua: -------------------------------------------------------------------------------- 1 | return function (req, res, go) 2 | -- Skip this layer for clients who don't send User-Agent headers. 3 | local userAgent = req.headers["user-agent"] 4 | if not userAgent then return go() end 5 | -- Run all inner layers first. 6 | go() 7 | -- And then log after everything is done 8 | print(string.format("%s %s %s %s", req.method, req.path, userAgent, res.code)) 9 | end 10 | -------------------------------------------------------------------------------- /libs/mime.lua: -------------------------------------------------------------------------------- 1 | 2 | local mime = {} 3 | local table = { 4 | ["3gp"] = "video/3gpp", 5 | a = "application/octet-stream", 6 | ai = "application/postscript", 7 | aif = "audio/x-aiff", 8 | aiff = "audio/x-aiff", 9 | asc = "application/pgp-signature", 10 | asf = "video/x-ms-asf", 11 | asm = "text/x-asm", 12 | asx = "video/x-ms-asf", 13 | atom = "application/atom+xml", 14 | au = "audio/basic", 15 | avi = "video/x-msvideo", 16 | bat = "application/x-msdownload", 17 | bin = "application/octet-stream", 18 | bmp = "image/bmp", 19 | bz2 = "application/x-bzip2", 20 | c = "text/x-c", 21 | cab = "application/vnd.ms-cab-compressed", 22 | cc = "text/x-c", 23 | chm = "application/vnd.ms-htmlhelp", 24 | class = "application/octet-stream", 25 | com = "application/x-msdownload", 26 | conf = "text/plain", 27 | cpp = "text/x-c", 28 | crt = "application/x-x509-ca-cert", 29 | css = "text/css", 30 | csv = "text/csv", 31 | cxx = "text/x-c", 32 | deb = "application/x-debian-package", 33 | der = "application/x-x509-ca-cert", 34 | diff = "text/x-diff", 35 | djv = "image/vnd.djvu", 36 | djvu = "image/vnd.djvu", 37 | dll = "application/x-msdownload", 38 | dmg = "application/octet-stream", 39 | doc = "application/msword", 40 | dot = "application/msword", 41 | dtd = "application/xml-dtd", 42 | dvi = "application/x-dvi", 43 | ear = "application/java-archive", 44 | eml = "message/rfc822", 45 | eps = "application/postscript", 46 | exe = "application/x-msdownload", 47 | f = "text/x-fortran", 48 | f77 = "text/x-fortran", 49 | f90 = "text/x-fortran", 50 | flv = "video/x-flv", 51 | ["for"] = "text/x-fortran", 52 | gem = "application/octet-stream", 53 | gemspec = "text/x-script.ruby", 54 | gif = "image/gif", 55 | gz = "application/x-gzip", 56 | h = "text/x-c", 57 | hh = "text/x-c", 58 | htm = "text/html", 59 | html = "text/html", 60 | ico = "image/vnd.microsoft.icon", 61 | ics = "text/calendar", 62 | ifb = "text/calendar", 63 | iso = "application/octet-stream", 64 | jar = "application/java-archive", 65 | java = "text/x-java-source", 66 | jnlp = "application/x-java-jnlp-file", 67 | jpeg = "image/jpeg", 68 | jpg = "image/jpeg", 69 | js = "application/javascript", 70 | json = "application/json", 71 | less = "text/css", 72 | log = "text/plain", 73 | lua = "text/x-lua", 74 | luac = "application/x-lua-bytecode", 75 | m3u = "audio/x-mpegurl", 76 | m4v = "video/mp4", 77 | man = "text/troff", 78 | manifest = "text/cache-manifest", 79 | markdown = "text/markdown", 80 | mathml = "application/mathml+xml", 81 | mbox = "application/mbox", 82 | mdoc = "text/troff", 83 | md = "text/markdown", 84 | me = "text/troff", 85 | mid = "audio/midi", 86 | midi = "audio/midi", 87 | mime = "message/rfc822", 88 | mml = "application/mathml+xml", 89 | mng = "video/x-mng", 90 | mov = "video/quicktime", 91 | mp3 = "audio/mpeg", 92 | mp4 = "video/mp4", 93 | mp4v = "video/mp4", 94 | mpeg = "video/mpeg", 95 | mpg = "video/mpeg", 96 | ms = "text/troff", 97 | msi = "application/x-msdownload", 98 | odp = "application/vnd.oasis.opendocument.presentation", 99 | ods = "application/vnd.oasis.opendocument.spreadsheet", 100 | odt = "application/vnd.oasis.opendocument.text", 101 | ogg = "application/ogg", 102 | p = "text/x-pascal", 103 | pas = "text/x-pascal", 104 | pbm = "image/x-portable-bitmap", 105 | pdf = "application/pdf", 106 | pem = "application/x-x509-ca-cert", 107 | pgm = "image/x-portable-graymap", 108 | pgp = "application/pgp-encrypted", 109 | pkg = "application/octet-stream", 110 | pl = "text/x-script.perl", 111 | pm = "text/x-script.perl-module", 112 | png = "image/png", 113 | pnm = "image/x-portable-anymap", 114 | ppm = "image/x-portable-pixmap", 115 | pps = "application/vnd.ms-powerpoint", 116 | ppt = "application/vnd.ms-powerpoint", 117 | ps = "application/postscript", 118 | psd = "image/vnd.adobe.photoshop", 119 | py = "text/x-script.python", 120 | qt = "video/quicktime", 121 | ra = "audio/x-pn-realaudio", 122 | rake = "text/x-script.ruby", 123 | ram = "audio/x-pn-realaudio", 124 | rar = "application/x-rar-compressed", 125 | rb = "text/x-script.ruby", 126 | rdf = "application/rdf+xml", 127 | roff = "text/troff", 128 | rpm = "application/x-redhat-package-manager", 129 | rss = "application/rss+xml", 130 | rtf = "application/rtf", 131 | ru = "text/x-script.ruby", 132 | s = "text/x-asm", 133 | sgm = "text/sgml", 134 | sgml = "text/sgml", 135 | sh = "application/x-sh", 136 | sig = "application/pgp-signature", 137 | snd = "audio/basic", 138 | so = "application/octet-stream", 139 | svg = "image/svg+xml", 140 | svgz = "image/svg+xml", 141 | swf = "application/x-shockwave-flash", 142 | t = "text/troff", 143 | tar = "application/x-tar", 144 | tbz = "application/x-bzip-compressed-tar", 145 | tci = "application/x-topcloud", 146 | tcl = "application/x-tcl", 147 | tex = "application/x-tex", 148 | texi = "application/x-texinfo", 149 | texinfo = "application/x-texinfo", 150 | text = "text/plain", 151 | tif = "image/tiff", 152 | tiff = "image/tiff", 153 | torrent = "application/x-bittorrent", 154 | tr = "text/troff", 155 | ttf = "application/x-font-ttf", 156 | txt = "text/plain", 157 | vcf = "text/x-vcard", 158 | vcs = "text/x-vcalendar", 159 | vrml = "model/vrml", 160 | war = "application/java-archive", 161 | wav = "audio/x-wav", 162 | webm = "video/webm", 163 | wma = "audio/x-ms-wma", 164 | wmv = "video/x-ms-wmv", 165 | wmx = "video/x-ms-wmx", 166 | wrl = "model/vrml", 167 | wsdl = "application/wsdl+xml", 168 | xbm = "image/x-xbitmap", 169 | xhtml = "application/xhtml+xml", 170 | xls = "application/vnd.ms-excel", 171 | xml = "application/xml", 172 | xpm = "image/x-xpixmap", 173 | xsl = "application/xml", 174 | xslt = "application/xslt+xml", 175 | yaml = "text/yaml", 176 | yml = "text/yaml", 177 | zip = "application/zip", 178 | } 179 | mime.table = table 180 | mime.default = "application/octet-stream" 181 | 182 | function mime.getType(path) 183 | return mime.table[path:lower():match("[^.]*$")] or mime.default 184 | end 185 | 186 | return mime 187 | 188 | -------------------------------------------------------------------------------- /libs/storage-fs.lua: -------------------------------------------------------------------------------- 1 | exports.name = "creationix/storage-fs" 2 | exports.version = "0.1.0" 3 | 4 | --[[ 5 | Low Level Storage Commands 6 | ========================== 7 | 8 | These are the filesystem abstractions needed by a git database 9 | 10 | storage.write(path, raw) - Write mutable data by path 11 | storage.put(path, raw) - Write immutable data by path 12 | storage.read(path) -> raw - Read mutable data by path (nil if not found) 13 | storage.delete(path) - Delete an entry (removes empty parent directories) 14 | storage.nodes(path) -> iter - Iterate over node children of path 15 | (empty iter if not found) 16 | storage.leaves(path) -> iter - Iterate over node children of path 17 | (empty iter if not found) 18 | ]] 19 | 20 | return function (fs) 21 | 22 | local storage = { fs = fs } 23 | 24 | 25 | local function dirname(path) 26 | return path:match("^(.*)/") or "" 27 | end 28 | 29 | -- Perform an atomic write (with temp file and rename) for mutable data 30 | function storage.write(path, data) 31 | local fd, success, err 32 | local tempPath = path .. "~" 33 | local tried = false 34 | while true do 35 | -- Try to open the file in write mode. 36 | fd, err = fs.open(tempPath, "w") 37 | if fd then break end 38 | if not tried and err:match("^ENOENT:") then 39 | -- If it doesn't exist, try to make the parent directory. 40 | assert(fs.mkdirp(dirname(path))) 41 | tried = true 42 | else 43 | assert(nil, err) 44 | end 45 | end 46 | success, err = fs.write(fd, data) 47 | if success then 48 | success, err = fs.fchmod(fd, 384) 49 | end 50 | fs.close(fd) 51 | if success then 52 | success, err = fs.rename(tempPath, path) 53 | end 54 | assert(success, err) 55 | end 56 | 57 | -- Write immutable data with an exclusive open. 58 | function storage.put(path, data) 59 | local fd, success, err 60 | local tried = false 61 | while true do 62 | -- Try to open the file in exclusive write mode. 63 | fd, err = fs.open(path, "wx") 64 | if fd then break end 65 | if err:match("^EEXIST:") then 66 | -- If it already exists, do nothing, it's immutable. 67 | return 68 | elseif not tried and err:match("^ENOENT:") then 69 | -- If it doesn't exist, try to make the parent directory. 70 | assert(fs.mkdirp(dirname(path))) 71 | tried = true 72 | else 73 | assert(nil, err) 74 | end 75 | end 76 | success, err = fs.write(fd, data) 77 | if success then 78 | success, err = fs.fchmod(fd, 256) 79 | end 80 | fs.close(fd) 81 | assert(success, err) 82 | end 83 | 84 | function storage.read(path) 85 | local data, err = fs.readFile(path) 86 | if data then return data end 87 | if err:match("^ENOENT:") then return end 88 | assert(data, err) 89 | end 90 | 91 | function storage.delete(path) 92 | assert(fs.unlink(path)) 93 | local dirPath = path 94 | while true do 95 | dirPath = dirname(dirPath) 96 | local iter = assert(fs.scandir(dirPath)) 97 | if iter() then return end 98 | assert(fs.rmdir(dirPath)) 99 | end 100 | end 101 | 102 | local function iter(path, filter) 103 | local iter, err = fs.scandir(path) 104 | if not iter then 105 | if err:match("^ENOENT:") then 106 | return function() end 107 | end 108 | assert(iter, err) 109 | end 110 | return function () 111 | while true do 112 | local item = iter() 113 | if not item then return end 114 | if item.type == filter then 115 | return item.name 116 | end 117 | end 118 | end 119 | end 120 | 121 | function storage.nodes(path) 122 | return iter(path, "directory") 123 | end 124 | 125 | function storage.leaves(path) 126 | return iter(path, "file") 127 | end 128 | 129 | return storage 130 | end 131 | -------------------------------------------------------------------------------- /libs/web-app.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Web App Framework 3 | 4 | Middleware Contract: 5 | 6 | function middleware(req, res, go) 7 | req.method 8 | req.path 9 | req.params 10 | req.headers 11 | req.version 12 | req.keepAlive 13 | req.body 14 | 15 | res.code 16 | res.headers 17 | res.body 18 | 19 | go() - Run next in chain, can tail call or wait for return and do more 20 | 21 | headers is a table/list with numerical headers. But you can also read and 22 | write headers using string keys, it will do case-insensitive compare for you. 23 | 24 | body can be a string or a stream. A stream is nothing more than a function 25 | you can call repeatedly to get new values. Returns nil when done. 26 | 27 | server 28 | .bind({ 29 | host = "0.0.0.0" 30 | port = 8080 31 | }) 32 | .bind({ 33 | host = "0.0.0.0", 34 | port = 8443, 35 | tls = true 36 | }) 37 | .route({ 38 | method = "GET", 39 | host = "^creationix.com", 40 | path = "/:path:" 41 | }, middleware) 42 | .use(middleware) 43 | .start() 44 | ]] 45 | 46 | local createServer = require('coro-tcp').createServer 47 | local wrapper = require('coro-wrapper') 48 | local readWrap, writeWrap = wrapper.reader, wrapper.writer 49 | local httpCodec = require('http-codec') 50 | 51 | local server = {} 52 | local handlers = {} 53 | local bindings = {} 54 | 55 | -- Provide a nice case insensitive interface to headers. 56 | local headerMeta = { 57 | __index = function (list, name) 58 | if type(name) ~= "string" then 59 | return rawget(list, name) 60 | end 61 | name = name:lower() 62 | for i = 1, #list do 63 | local key, value = unpack(list[i]) 64 | if key:lower() == name then return value end 65 | end 66 | end, 67 | __newindex = function (list, name, value) 68 | if type(name) ~= "string" then 69 | return rawset(list, name, value) 70 | end 71 | local lowerName = name:lower() 72 | for i = 1, #list do 73 | local key = list[i][1] 74 | if key:lower() == lowerName then 75 | if value == nil then 76 | table.remove(list, i) 77 | else 78 | list[i] = {name, tostring(value)} 79 | end 80 | return 81 | end 82 | end 83 | if value == nil then return end 84 | rawset(list, #list + 1, {name, tostring(value)}) 85 | end, 86 | } 87 | 88 | local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])' 89 | local function escape(str) 90 | return str:gsub(quotepattern, "%%%1") 91 | end 92 | 93 | local function compileGlob(glob) 94 | local parts = {"^"} 95 | for a, b in glob:gmatch("([^*]*)(%**)") do 96 | if #a > 0 then 97 | parts[#parts + 1] = escape(a) 98 | end 99 | if #b > 0 then 100 | parts[#parts + 1] = "(.*)" 101 | end 102 | end 103 | parts[#parts + 1] = "$" 104 | local pattern = table.concat(parts) 105 | return function (string) 106 | return string:match(pattern) 107 | end 108 | end 109 | 110 | local function compileRoute(route) 111 | local parts = {"^"} 112 | local names = {} 113 | for a, b, c, d in route:gmatch("([^:]*):([_%a][_%w]*)(:?)([^:]*)") do 114 | if #a > 0 then 115 | parts[#parts + 1] = escape(a) 116 | end 117 | if #c > 0 then 118 | parts[#parts + 1] = "(.*)" 119 | else 120 | parts[#parts + 1] = "([^/]*)" 121 | end 122 | names[#names + 1] = b 123 | if #d > 0 then 124 | parts[#parts + 1] = escape(d) 125 | end 126 | end 127 | if #parts == 1 then 128 | return function (string) 129 | if string == route then return {} end 130 | end 131 | end 132 | parts[#parts + 1] = "$" 133 | local pattern = table.concat(parts) 134 | return function (string) 135 | local matches = {string:match(pattern)} 136 | if #matches > 0 then 137 | local results = {} 138 | for i = 1, #matches do 139 | results[i] = matches[i] 140 | results[names[i]] = matches[i] 141 | end 142 | return results 143 | end 144 | end 145 | end 146 | 147 | 148 | local function handleRequest(head, input) 149 | local req = { 150 | method = head.method, 151 | path = head.path, 152 | headers = setmetatable({}, headerMeta), 153 | version = head.version, 154 | keepAlive = head.keepAlive, 155 | body = input 156 | } 157 | for i = 1, #head do 158 | req.headers[i] = head[i] 159 | end 160 | 161 | local res = { 162 | code = 404, 163 | headers = setmetatable({}, headerMeta), 164 | body = "Not Found\n", 165 | } 166 | 167 | local function run(i) 168 | local success, err = pcall(function () 169 | i = i or 1 170 | local go = i < #handlers 171 | and function () 172 | return run(i + 1) 173 | end 174 | or function () end 175 | return handlers[i](req, res, go) 176 | end) 177 | if not success then 178 | res.code = 500 179 | res.headers = setmetatable({}, headerMeta) 180 | res.body = err 181 | print(err) 182 | end 183 | end 184 | run(1) 185 | 186 | local out = { 187 | code = res.code, 188 | keepAlive = res.keepAlive, 189 | } 190 | for i = 1, #res.headers do 191 | out[i] = res.headers[i] 192 | end 193 | return out, res.body 194 | end 195 | 196 | local function handleConnection(rawRead, rawWrite) 197 | 198 | -- Speak in HTTP events 199 | local read = readWrap(rawRead, httpCodec.decoder()) 200 | local write = writeWrap(rawWrite, httpCodec.encoder()) 201 | 202 | for req in read do 203 | local parts = {} 204 | for chunk in read do 205 | if #chunk > 0 then 206 | parts[#parts + 1] = chunk 207 | else 208 | break 209 | end 210 | end 211 | req.parts = #parts > 0 and table.concat(parts) or nil 212 | local res, body = handleRequest(req) 213 | write(res) 214 | write(body) 215 | if not (res.keepAlive and req.keepAlive) then 216 | break 217 | end 218 | end 219 | write() 220 | 221 | end 222 | 223 | function server.bind(options) 224 | bindings[#bindings + 1] = options 225 | return server 226 | end 227 | 228 | function server.use(handler) 229 | handlers[#handlers + 1] = handler 230 | return server 231 | end 232 | 233 | function server.route(options, handler) 234 | local method = options.method 235 | local path = options.path and compileRoute(options.path) 236 | local host = options.host and compileGlob(options.host) 237 | handlers[#handlers + 1] = function (req, res, go) 238 | if method and req.method ~= method then return go() end 239 | if host and not (req.headers.host and host(req.headers.host)) then return go() end 240 | local params 241 | if path then 242 | params = path(req.path) 243 | if not params then return go() end 244 | end 245 | req.params = params or {} 246 | return handler(req, res, go) 247 | end 248 | return server 249 | end 250 | 251 | function server.start() 252 | for i = 1, #bindings do 253 | local options = bindings[i] 254 | -- TODO: handle options.tls 255 | createServer(options.host, options.port, handleConnection) 256 | print("HTTP server listening at http://" .. options.host .. ":" .. options.port .. "/") 257 | end 258 | return server 259 | end 260 | 261 | return server 262 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local luvi = require('luvi') 2 | luvi.bundle.register('require', "deps/require.lua") 3 | local require = require('require')("bundle:main.lua") 4 | _G.p = require('pretty-print').prettyPrint 5 | coroutine.wrap(require)('./server') 6 | require('uv').run() 7 | -------------------------------------------------------------------------------- /package.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = "creationix/rye", 3 | private = true, 4 | version = "0.0.1", 5 | dependencies = { 6 | "luvit/require@0.2.1", 7 | "luvit/http-codec@0.1.4", 8 | "luvit/pretty-print@0.1.0", 9 | "luvit/json@0.1.0", 10 | "creationix/git@0.1.1", 11 | "creationix/hex-bin@1.0.0", 12 | "creationix/coro-tcp@1.0.4", 13 | "creationix/coro-fs@1.2.3", 14 | "creationix/coro-wrapper@0.1.0", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | -- Load the git-serve web library 2 | local gitServe = require('git-serve') 3 | 4 | -- Grab an internal library from luvi for convenience 5 | local bundle = require('luvi').bundle 6 | 7 | -- Load the git repo into a coroutine based filesystem abstraction 8 | -- Wrap it some midl-level sugar to make it suitable for git 9 | -- Create a git database interface around the storage layer 10 | local fs = require('coro-fs') 11 | local storage = require('storage-fs')(fs.chroot('sites.git')) 12 | local db = require('git-fs')(storage) 13 | -- Add an in-memory cache for all git objects under 200k with a total 14 | -- max cache size of 2mb 15 | require('git-hash-cache')(db, 200000, 2000000) 16 | 17 | -- Cache all ref lookups for 1000ms 18 | require('git-ref-cache')(db, 1000) 19 | 20 | -- Create three instances of the gitServe app using different refs in our 21 | -- local git repo. A cron job or post commit hook will `git fetch ...` these 22 | -- to keep the database up to date. Eventually rye can poll for updates on a 23 | -- smart interval based on requests. 24 | local luvitApp = gitServe(db, "refs/remotes/luvit.io/master") 25 | local exploderApp = gitServe(db, "refs/remotes/exploder.creationix.com/master") 26 | local creationixApp = gitServe(db, "refs/remotes/creationix.com/master") 27 | local conquestApp = gitServe(db, "refs/remotes/conquest.creationix.com/master") 28 | 29 | -- Configure the web app 30 | require('web-app') 31 | 32 | -- Declare the host and port to bind to. 33 | .bind({host="0.0.0.0", port=8080}) 34 | 35 | -- Set an outer middleware for logging requests and responses 36 | .use(require('logger')) 37 | 38 | -- This adds missing headers, and tries to do automatic cleanup. 39 | .use(require('auto-headers')) 40 | 41 | -- A caching proxy layer for backends supporting Etags 42 | .use(require('etag-cache')) 43 | 44 | .route({ method="GET", path="/" }, function (req, res, go) 45 | -- Render a dynamic welcome page for clients with a user-agent 46 | local userAgent = req.headers["User-Agent"] 47 | if not userAgent then return go() end 48 | local template = bundle.readfile("index.html") 49 | res.code = 200 50 | res.body = template:gsub("%%USER_AGENT%%", userAgent) 51 | res.headers["Content-Type"] = "text/html" 52 | end) 53 | 54 | -- Mount the git app on three virtual hosts 55 | .route({ method="GET", host="luvit.localdomain*" }, luvitApp) 56 | .route({ method="GET", host="exploder.localdomain*" }, exploderApp) 57 | .route({ method="GET", host="creationix.localdomain*" }, creationixApp) 58 | .route({ method="GET", host="conquest.localdomain*" }, conquestApp) 59 | 60 | -- Mount them again, but on subpaths instead of virtual hosts 61 | .route({ method="GET", path="/luvit/:path:" }, luvitApp) 62 | .route({ method="GET", path="/exploder/:path:" }, exploderApp) 63 | .route({ method="GET", path="/creationix/:path:" }, creationixApp) 64 | .route({ method="GET", path="/conquest/:path:" }, conquestApp) 65 | 66 | -- Bind the ports, start the server and begin listening for and accepting connections. 67 | .start() 68 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | rm -rf sites.git 4 | git init --bare sites.git 5 | cd sites.git 6 | git remote add luvit.io https://github.com/luvit/luvit.io.git 7 | git fetch luvit.io 8 | git remote add creationix.com https://github.com/creationix/creationix.com.git 9 | git fetch creationix.com 10 | git remote add exploder.creationix.com https://github.com/creationix/exploder.git 11 | git fetch exploder.creationix.com 12 | git remote add conquest.creationix.com https://github.com/creationix/conquest.git 13 | git fetch conquest.creationix.com 14 | -------------------------------------------------------------------------------- /test-main.lua: -------------------------------------------------------------------------------- 1 | local luvi = require('luvi') 2 | luvi.bundle.register('require', "deps/require.lua") 3 | local require = require('require')("bundle:main.lua") 4 | _G.p = require('pretty-print').prettyPrint 5 | coroutine.wrap(function () 6 | 7 | local fs = require('coro-fs') 8 | local storage = require('storage-fs')(fs.chroot('sites.git')) 9 | local db = require('git-fs')(storage) 10 | require('git-hash-cache')(db, 200000, 2000000) 11 | 12 | local modes = require('git').modes 13 | 14 | local function walkTree(hash) 15 | local tree = db.loadAs("tree", hash) 16 | for i = 1, #tree do 17 | local entry = tree[i] 18 | if entry.mode == modes.tree then 19 | walkTree(entry.hash) 20 | elseif modes.isBlob(entry.mode) then 21 | db.loadAs("blob", entry.hash) 22 | end 23 | end 24 | end 25 | 26 | local function walkCommit(hash) 27 | -- print("commit", hash) 28 | local commit = db.loadAs("commit", hash) 29 | -- print(commit.message) 30 | walkTree(commit.tree) 31 | for i = 1, #commit.parents do 32 | walkCommit(commit.parents[i]) 33 | end 34 | end 35 | 36 | walkCommit("refs/remotes/luvit.io/master") 37 | walkCommit("refs/remotes/exploder.creationix.com/master") 38 | walkCommit("refs/remotes/creationix.com/master") 39 | walkCommit("refs/remotes/conquest.creationix.com/master") 40 | 41 | end)() 42 | require('uv').run() 43 | 44 | --------------------------------------------------------------------------------