4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 22:19:26 +00:00

Compare commits

...

46 Commits

Author SHA1 Message Date
Wim
6d8cccccf1 Release v1.14.4 2019-04-23 23:32:19 +02:00
Wim
dd63438e7b Update changelog 2019-04-23 23:31:45 +02:00
Wim
5f5d6c6e8a Disable user lookups on delete messages (slack) (#812) 2019-04-23 23:29:52 +02:00
aefa8a9341 Add lacking clean-up in Slack synchronisation (#811) 2019-04-23 23:28:35 +02:00
Wim
1c3e764d57 Use paging in initUser and UpdateUsers (mattermost) 2019-04-23 23:28:28 +02:00
Wim
0b03076a9d Handle unthreaded messages (mattermost). Fixes #803 2019-04-23 23:28:13 +02:00
Wim
1e6a2bc8f7 Fix panic on nil message.Post (mattermost). Fixes #804 2019-04-23 23:27:56 +02:00
Wim
82fe80e52f Add Id to EditMessage (mattermost). Fixes #802 2019-04-23 23:27:49 +02:00
Wim
db012bd9b7 Release v1.14.3 2019-04-19 18:16:35 +02:00
Wim
dd2374158b Update changelog 2019-04-19 18:15:09 +02:00
Wim
6693157258 Fix deadlock on reconnect (irc). Closes #757 2019-04-19 18:13:41 +02:00
Wim
e4d73b29a1 Release v1.14.2 2019-04-06 23:29:49 +02:00
Wim
8a875f292e Revert fix for #722. Closes #781
Revert "Fix typo"

This reverts commit dffd67eb31.

Revert "Handle quit message relay better on gateways with one channel on the irc bridge #722"

This reverts commit 240559581a.

Revert "Support quits from irc correctly. Fixes #722 (#724)"

This reverts commit d76a04bd0a.
2019-04-06 23:12:48 +02:00
Wim
60a85621ea Return when not connected and drop a message (irc). Fixes #786 2019-04-06 22:34:41 +02:00
Wim
115d20373c Update tengo vendor and load the stdlib. Fixes #789 (#792) 2019-04-06 22:18:25 +02:00
Wim
cdf33e5748 Use default nick if none specified (irc). Fixes #785 2019-04-05 00:17:46 +02:00
Wim
01d0a9f412 Handle nil message (telegram). Fixes #777 2019-04-05 00:04:08 +02:00
Wim
8cc2d3b4fe Revert "Bail if any vars are nil, not if all (telegram) (#778)"
This reverts commit efd2c99862.
2019-04-05 00:02:26 +02:00
Wim
aba9e4f3be Fix travis before_deploy 2019-04-04 23:22:56 +02:00
Wim
4d575ba13a Fix travis deploy condition and update to golangci-lint v1.16 2019-04-04 23:05:58 +02:00
7f0e4ad448 Add CI fixes and improvements (#780)
* Update GolangCI-lint and lint config

The `algo` parameter for the `unparam` linter has been removed and we
should thus no longer specify it. Also, bumping the GolangCI-lint
version to the latest available minor release.

See: mvdan/unparam@e6a6d1c51b

* Fix and improve bintray CI script

* Further CI setup improvements

* Split-out CI steps into stand-alone scripts
2019-04-04 22:54:51 +02:00
Wim
17cc14a9d2 Send user_added and removed event through message channel (mattermost) 2019-04-02 00:15:58 +02:00
Wim
1f8016182c Return channelId for other channeltypes too (mattermost) 2019-04-01 22:50:19 +02:00
Wim
caf9ef2c4b Bump travis to go 1.12.x 2019-03-27 23:22:55 +01:00
Wim
64b57f2da3 Ignore message_replied and hidden messages (slack). Fixes #709 (#779) 2019-03-27 22:54:18 +01:00
efd2c99862 Bail if any vars are nil, not if all (telegram) (#778) 2019-03-27 21:00:57 +01:00
Wim
cc05ba8907 Thank DigitalOcean (https://digitalocean.com) for another year of sponsorship 2019-03-25 21:13:11 +01:00
Wim
16763b715a Look up #channel too (rocketchat). Fix #773 (#775) 2019-03-24 20:15:15 +01:00
Wim
ffaa598796 Bump version 2019-03-24 19:52:31 +01:00
Wim
858e16d34f Release v1.14.1 2019-03-21 21:07:11 +01:00
Wim
a60e62efb1 Update doc wrt rocketchat api issue 2019-03-21 21:05:27 +01:00
97f9d4be67 Fix double unlock (slack) (#771) 2019-03-21 17:30:28 +01:00
Wim
fa4eec41f7 Release v1.14.0 2019-03-20 23:30:03 +01:00
Wim
77516c97db Allow the # in rocketchat channels (backward compatible) (#769) 2019-03-20 23:19:27 +01:00
Wim
cba01f0865 Update rocketchat documentation 2019-03-20 23:18:40 +01:00
8b754017ca Fix race-condition in populateUser() (#767)
Fix the root-cause of #759 by introducing synchronisation points for
individual user fetches.
2019-03-20 22:54:31 +01:00
Wim
a27600046e Fix regression for legacy slack by #766 (#768) 2019-03-20 22:52:23 +01:00
fb2667631d Refactor channel and user management (slack) (#766) 2019-03-15 21:23:09 +01:00
b638f7037a Force Slack link unfurling (#763) 2019-03-12 22:56:43 +01:00
74699a8262 Split-out Slack user and channel management (#762) 2019-03-12 22:52:36 +01:00
eabf2a4582 Check module files in CI run (#761) 2019-03-12 22:47:18 +01:00
Wim
325d62b41c Update vendor d5/tengo 2019-03-05 23:10:45 +01:00
Wim
e955a056e2 Trim <p> and </p> tags (matrix). Closes #686 (#753) 2019-03-03 00:29:29 +01:00
Wim
723f8c5fd5 Only build travis on master branch 2019-03-03 00:24:49 +01:00
Wim
a16137f53f Bump version 2019-03-02 23:48:46 +01:00
Wim
d60b8b97f9 Add related projects to README 2019-03-02 23:48:30 +01:00
99 changed files with 4331 additions and 2140 deletions

View File

@ -7,7 +7,7 @@ run:
# concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m
deadline: 2m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
@ -105,10 +105,6 @@ linters-settings:
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
# and rta for programs with main packages. Default is cha.
algo: rta
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
@ -158,7 +154,6 @@ linters-settings:
- regexpMust
- singleCaseSwitch
- sloppyLen
- sloppyReassign
- switchTrue
- typeSwitchVar
- typeUnparen

View File

@ -1,63 +1,55 @@
language: go
go:
- 1.11.x
go_import_path: github.com/42wim/matterbridge
# we have everything vendored
# We have everything vendored so this helps TravisCI not run `go get ...`.
install: true
git:
depth: 200
env:
global:
- GOOS=linux GOARCH=amd64
- GOLANGCI_VERSION="v1.14.0"
matrix:
# It's ok if our code fails on unstable development versions of Go.
allow_failures:
- go: tip
# Don't wait for tip tests to finish. Mark the test run green if the
# tests pass on the stable versions of Go.
fast_finish: true
notifications:
email: false
email: false
before_script:
# Get version info from tags.
- MY_VERSION="$(git describe --tags)"
# Retrieve the golangci-lint linter binary.
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
# Retrieve and prepare CodeClimate's test coverage reporter.
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
branches:
only:
- master
script:
# Run the linter.
- golangci-lint run
# Run all the tests with the race detector and generate coverage.
- go test -v -race -coverprofile c.out ./...
# Run the build script to generate the necessary binaries and images.
- /bin/bash ci/bintray.sh
jobs:
include:
- stage: lint
# Run linting in one Go environment only.
script: ./ci/lint.sh
go: 1.12.x
env:
- GO111MODULE=on
- GOLANGCI_VERSION="v1.16.0"
- stage: test
# Run tests in a combination of Go environments.
script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=off
- script: ./ci/test.sh
go: 1.11.x
env:
- GO111MODULE=on
- script: ./ci/test.sh
go: 1.12.x
env:
- GO111MODULE=on
- REPORT_COVERAGE=1
- BINDEPLOY=1
after_script:
# Upload test coverage to CodeClimate.
- ./cc-test-reporter after-build --exit-code ${TRAVIS_TEST_RESULT}
before_deploy: /bin/bash ci/bintray.sh
deploy:
on:
all_branches: true
provider: bintray
on:
all_branches: true
condition: $BINDEPLOY = 1
provider: bintray
edge:
branch: v1.8.47
file: ci/deploy.json
user: 42wim
on:
all_branches: true
key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@ -35,6 +35,12 @@
**Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px">
</a>
</p>
### Table of Contents
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Natively supported](#natively-supported)
@ -88,6 +94,7 @@
* [Minecraft](https://github.com/elytra/MatterLink)
* [Reddit](https://github.com/bonehurtingjuice/mattereddit)
* [Facebook messenger](https://github.com/VictorNine/fbridge)
* [Discourse](https://github.com/DeclanHoare/matterbabble)
### API
The API is very basic at the moment.
@ -99,6 +106,7 @@ Used by the projects below. Feel free to make a PR to add your project to this l
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
## Chat with us
@ -121,7 +129,7 @@ See https://github.com/42wim/matterbridge/wiki
## Installing
### Binaries
* Latest stable release [v1.13.1](https://github.com/42wim/matterbridge/releases/latest)
* Latest stable release [v1.14.2](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
### Packages
@ -247,6 +255,8 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
* [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
* [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
* [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
* [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
* [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
## Articles
* [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
@ -260,7 +270,13 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
* https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
## Thanks
[![Digitalocean](https://snag.gy/3LVifX.jpg)](https://www.digitalocean.com/) for sponsoring demo/testing droplets.
<p>This project is supported by:</p>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
Matterbridge wouldn't exist without these libraries:
* discord - https://github.com/bwmarrin/discordgo

View File

@ -178,7 +178,10 @@ func ClipMessage(text string, length int) string {
func ParseMarkdown(input string) string {
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
return (md.RenderToString([]byte(input)))
res := md.RenderToString([]byte(input))
res = strings.TrimPrefix(res, "<p>")
res = strings.TrimSuffix(res, "</p>\n")
return res
}
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)

View File

@ -92,10 +92,6 @@ func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
return
}
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
// QUIT isn't channel bound, happens for all channels on the bridge
if event.Command == "QUIT" {
channel = ""
}
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg
@ -160,7 +156,10 @@ func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
b.handleNickServ()
b.handleRunCommands()
// we are now fully connected
b.connected <- nil
// only send on first connection
if b.FirstConnection {
b.connected <- nil
}
}
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {

View File

@ -137,6 +137,7 @@ func (b *Birc) Send(msg config.Message) (string, error) {
// we can be in between reconnects #385
if !b.i.IsConnected() {
b.Log.Error("Not connected to server, dropping message")
return "", nil
}
// Execute a command
@ -231,7 +232,7 @@ func (b *Birc) getClient() (*girc.Client, error) {
// fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) {
if len(user) == 1 {
if len(user) == 1 || len(user) == 0 {
user = "matterbridge"
break
}

View File

@ -186,6 +186,12 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
return true
}
// Ignore non-post messages
if message.Post == nil {
b.Log.Debugf("ignoring nil message.Post: %#v", message)
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {

View File

@ -121,6 +121,12 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {

View File

@ -95,7 +95,7 @@ func (b *Brocketchat) getChannelID(name string) string {
b.RLock()
defer b.RUnlock()
for k, v := range b.channelMap {
if v == name {
if v == name || v == "#"+name {
return k
}
}

View File

@ -2,6 +2,7 @@ package brocketchat
import (
"errors"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
@ -85,14 +86,14 @@ func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil {
return nil
}
id, err := b.c.GetChannelId(channel.Name)
id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
if err != nil {
return err
}
b.Lock()
b.channelMap[id] = channel.Name
b.Unlock()
mychannel := &models.Channel{ID: id, Name: channel.Name}
mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
if err := b.c.JoinChannel(id); err != nil {
return err
}
@ -103,6 +104,8 @@ func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
}
func (b *Brocketchat) Send(msg config.Message) (string, error) {
// strip the # if people has set this
msg.Channel = strings.TrimPrefix(msg.Channel, "#")
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Delete message
@ -131,6 +134,8 @@ func (b *Brocketchat) Send(msg config.Message) (string, error) {
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
// strip the # if people has set this
rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
smsg := &models.Message{
RoomID: b.getChannelID(rmsg.Channel),
Msg: rmsg.Username + rmsg.Text,

View File

@ -22,20 +22,20 @@ func (b *Bslack) handleSlack() {
time.Sleep(time.Second)
b.Log.Debug("Start listening for Slack messages")
for message := range messages {
if message.Event != config.EventUserTyping {
// don't do any action on deleted/typing messages
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete {
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
// cleanup the message
message.Text = b.replaceMention(message.Text)
message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text)
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.users.getAvatar(message.UserID)
}
// cleanup the message
message.Text = b.replaceMention(message.Text)
message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text)
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.getAvatar(message.UserID)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
@ -75,20 +75,17 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
// When we join a channel we update the full list of users as
// well as the information for the channel that we joined as this
// should now tell that we are a member of it.
b.channelsMutex.Lock()
b.channelsByID[ev.Channel.ID] = &ev.Channel
b.channelsByName[ev.Channel.Name] = &ev.Channel
b.channelsMutex.Unlock()
b.channels.registerChannel(ev.Channel)
case *slack.ConnectedEvent:
b.si = ev.Info
b.populateChannels(true)
b.populateUsers(true)
b.channels.populateChannels(true)
b.users.populateUsers(true)
case *slack.InvalidAuthEvent:
b.Log.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent:
b.populateUser(ev.User)
b.users.populateUser(ev.User)
case *slack.LatencyReport:
continue
default:
@ -133,12 +130,18 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return true
}
// It seems ev.SubMessage.Edited == nil when slack unfurls.
// Do not forward these messages. See Github issue #266.
if ev.SubMessage != nil &&
ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
ev.SubMessage.Edited == nil {
return true
if ev.SubMessage != nil {
// It seems ev.SubMessage.Edited == nil when slack unfurls.
// Do not forward these messages. See Github issue #266.
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
ev.SubMessage.Edited == nil {
return true
}
// see hidden subtypes at https://api.slack.com/events/message
// these messages are sent when we add a message to a thread #709
if ev.SubType == "message_replied" && ev.Hidden {
return true
}
}
if len(ev.Files) > 0 {
@ -210,7 +213,7 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose:
b.populateChannels(false)
b.channels.populateChannels(false)
rmsg.Event = config.EventTopicChange
case sMessageChanged:
rmsg.Text = ev.SubMessage.Text
@ -266,7 +269,7 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
}
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
channelInfo, err := b.getChannelByID(ev.Channel)
channelInfo, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
@ -316,36 +319,7 @@ func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
return false
}
cMembers := config.ChannelMembers{}
b.channelMembersMutex.RLock()
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := b.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
cMember := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
cMembers = append(cMembers, cMember)
}
}
b.channelMembersMutex.RUnlock()
cMembers := b.channels.getChannelMembers(b.users)
extra := make(map[string][]interface{})
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)

View File

@ -1,7 +1,6 @@
package bslack
import (
"context"
"fmt"
"regexp"
"strings"
@ -9,225 +8,14 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
)
func (b *Bslack) getUser(id string) *slack.User {
b.usersMutex.RLock()
user, ok := b.users[id]
b.usersMutex.RUnlock()
if ok {
return user
}
b.populateUser(id)
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *Bslack) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.Log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *Bslack) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *Bslack) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *Bslack) getChannelByName(name string) (*slack.Channel, error) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *Bslack) getChannelByID(ID string) (*slack.Channel, error) {
return b.getChannelBy(ID, b.channelsByID)
}
func (b *Bslack) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := lookupMap[lookupKey]; ok {
return channel, nil
}
return nil, fmt.Errorf("%s: channel %s not found", b.Account, lookupKey)
}
const minimumRefreshInterval = 10 * time.Second
func (b *Bslack) populateUser(userID string) {
b.usersMutex.RLock()
_, exists := b.users[userID]
b.usersMutex.RUnlock()
if exists {
// already in cache
return
}
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.Log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
b.users[userID] = user
b.usersMutex.Unlock()
}
func (b *Bslack) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestUserRefresh) || b.refreshInProgress) {
b.Log.Debugf("Not refreshing user list as it was done less than %v ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
count := 0
for {
var err error
pagination, err = pagination.Next(context.Background())
time.Sleep(time.Second)
if err != nil {
if pagination.Done(err) {
break
}
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
b.Log.Debugf("getting %d users", len(pagination.Users))
count++
// more > 2000 users, slack will complain and ratelimit. break
if count > 10 {
b.Log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
break
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestUserRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
func (b *Bslack) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestChannelRefresh) || b.refreshInProgress) {
b.Log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.",
minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
newChannelMembers := make(map[string][]string)
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"},
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
// also find all the members in every channel
// comment for now, issues on big slacks
/*
members, err := b.getUsersInConversation(channels[i].ID)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channel members: %#v", err)
return
}
continue
}
newChannelMembers[channels[i].ID] = members
*/
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.channelMembersMutex.Lock()
defer b.channelMembersMutex.Unlock()
b.channelMembers = newChannelMembers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestChannelRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// router before we apply message-dependent modifications.
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
channel, err := b.getChannelByID(ev.Channel)
channel, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
@ -289,7 +77,7 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
return nil
}
user := b.getUser(userID)
user := b.users.getUser(userID)
if user == nil {
return fmt.Errorf("could not find information for user with id %s", ev.User)
}
@ -315,7 +103,7 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
break
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Could not retrieve bot information: %#v", err)
return err
}
@ -360,7 +148,7 @@ func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
func (b *Bslack) replaceMention(text string) string {
replaceFunc := func(match string) string {
userID := strings.Trim(match, "@<>")
if username := b.getUsername(userID); userID != "" {
if username := b.users.getUsername(userID); userID != "" {
return "@" + username
}
return match
@ -404,16 +192,6 @@ func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```")
}
func (b *Bslack) handleRateLimit(err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
b.Log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}
// getUsersInConversation returns an array of userIDs that are members of channelID
func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
channelMembers := []string{}
@ -424,7 +202,7 @@ func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
}
continue
@ -439,3 +217,13 @@ func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
}
return channelMembers, nil
}
func handleRateLimit(log *logrus.Entry, err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}

View File

@ -55,14 +55,18 @@ func (b *BLegacy) Connect() error {
})
if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (receiving)")
b.sc = slack.New(b.GetString(tokenConfig))
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
} else if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (sending and receiving)")
b.sc = slack.New(b.GetString(tokenConfig))
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()

View File

@ -30,20 +30,8 @@ type Bslack struct {
uuid string
useChannelID bool
users map[string]*slack.User
usersMutex sync.RWMutex
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestChannelRefresh time.Time
earliestUserRefresh time.Time
refreshMutex sync.Mutex
channels *channels
users *users
}
const (
@ -94,14 +82,9 @@ func newBridge(cfg *bridge.Config) *Bslack {
cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
}
b := &Bslack{
Config: cfg,
uuid: xid.New().String(),
cache: newCache,
users: map[string]*slack.User{},
channelsByID: map[string]*slack.Channel{},
channelsByName: map[string]*slack.Channel{},
earliestChannelRefresh: time.Now(),
earliestUserRefresh: time.Now(),
Config: cfg,
uuid: xid.New().String(),
cache: newCache,
}
return b
}
@ -121,7 +104,12 @@ func (b *Bslack) Connect() error {
// If we have a token we use the Slack websocket-based RTM for both sending and receiving.
if token := b.GetString(tokenConfig); token != "" {
b.Log.Info("Connecting using token")
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
@ -163,9 +151,9 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
return nil
}
b.populateChannels(false)
b.channels.populateChannels(false)
channelInfo, err := b.getChannel(channel.Name)
channelInfo, err := b.channels.getChannel(channel.Name)
if err != nil {
return fmt.Errorf("could not join channel: %#v", err)
}
@ -275,7 +263,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return "", nil
}
channelInfo, err := b.getChannel(msg.Channel)
channelInfo, err := b.channels.getChannel(msg.Channel)
if err != nil {
return "", fmt.Errorf("could not send message: %v", err)
}
@ -351,7 +339,7 @@ func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Ch
if err == nil {
return nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
return err
}
}
@ -392,7 +380,7 @@ func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel)
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
return true, err
}
@ -411,7 +399,7 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
return true, err
}
@ -424,14 +412,18 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
return "", nil
}
messageOptions := b.prepareMessageOptions(msg)
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
messageOptions = append(
messageOptions,
slack.MsgOptionText(msg.Text, false),
slack.MsgOptionEnableLinkUnfurl(),
)
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
return id, nil
}
if err = b.handleRateLimit(err); err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
return "", err
}

View File

@ -0,0 +1,336 @@
package bslack
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
)
const minimumRefreshInterval = 10 * time.Second
type users struct {
log *logrus.Entry
sc *slack.Client
users map[string]*slack.User
usersMutex sync.RWMutex
usersSyncPoints map[string]chan struct{}
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newUserManager(log *logrus.Entry, sc *slack.Client) *users {
return &users{
log: log,
sc: sc,
users: make(map[string]*slack.User),
usersSyncPoints: make(map[string]chan struct{}),
earliestRefresh: time.Now(),
}
}
func (b *users) getUser(id string) *slack.User {
b.usersMutex.RLock()
user, ok := b.users[id]
b.usersMutex.RUnlock()
if ok {
return user
}
b.populateUser(id)
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *users) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *users) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *users) populateUser(userID string) {
for {
b.usersMutex.Lock()
_, exists := b.users[userID]
if exists {
// already in cache
b.usersMutex.Unlock()
return
}
if syncPoint, ok := b.usersSyncPoints[userID]; ok {
// Another goroutine is already populating this user for us so wait on it to finish.
b.usersMutex.Unlock()
<-syncPoint
// We do not return and iterate again to check that the entry does indeed exist
// in case the previous query failed for some reason.
} else {
b.usersSyncPoints[userID] = make(chan struct{})
defer func() {
// Wake up any waiting goroutines and remove the synchronization point.
close(b.usersSyncPoints[userID])
delete(b.usersSyncPoints, userID)
}()
break
}
}
// Do not hold the lock while fetching information from Slack
// as this might take an unbounded amount of time.
b.usersMutex.Unlock()
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
// Register user information.
b.users[userID] = user
}
func (b *users) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing user list as it was done less than %v ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
count := 0
for {
var err error
pagination, err = pagination.Next(context.Background())
time.Sleep(time.Second)
if err != nil {
if pagination.Done(err) {
break
}
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
b.log.Debugf("getting %d users", len(pagination.Users))
count++
// more > 2000 users, slack will complain and ratelimit. break
if count > 10 {
b.log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
break
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
type channels struct {
log *logrus.Entry
sc *slack.Client
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newChannelManager(log *logrus.Entry, sc *slack.Client) *channels {
return &channels{
log: log,
sc: sc,
channelsByID: make(map[string]*slack.Channel),
channelsByName: make(map[string]*slack.Channel),
earliestRefresh: time.Now(),
}
}
func (b *channels) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *channels) getChannelByName(name string) (*slack.Channel, error) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *channels) getChannelByID(id string) (*slack.Channel, error) {
return b.getChannelBy(id, b.channelsByID)
}
func (b *channels) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := lookupMap[lookupKey]; ok {
return channel, nil
}
return nil, fmt.Errorf("channel %s not found", lookupKey)
}
func (b *channels) getChannelMembers(users *users) config.ChannelMembers {
b.channelMembersMutex.RLock()
defer b.channelMembersMutex.RUnlock()
membersInfo := config.ChannelMembers{}
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := users.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
memberInfo := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
membersInfo = append(membersInfo, memberInfo)
}
}
return membersInfo
}
func (b *channels) registerChannel(channel slack.Channel) {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID[channel.ID] = &channel
b.channelsByName[channel.Name] = &channel
}
func (b *channels) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
newChannelMembers := make(map[string][]string)
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
Types: []string{"public_channel,private_channel"},
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
// also find all the members in every channel
// comment for now, issues on big slacks
/*
members, err := b.getUsersInConversation(channels[i].ID)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channel members: %#v", err)
return
}
continue
}
newChannelMembers[channels[i].ID] = members
*/
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.channelMembersMutex.Lock()
defer b.channelMembersMutex.Unlock()
b.channelMembers = newChannelMembers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}

View File

@ -125,6 +125,11 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handle groups
message = b.handleGroups(&rmsg, message, update)
if message == nil {
b.Log.Error("message is nil, this shouldn't happen.")
continue
}
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)

View File

@ -1,3 +1,37 @@
# v1.14.4
## Bugfix
* mattermost: Add Id to EditMessage (mattermost). Fixes #802
* mattermost: Fix panic on nil message.Post (mattermost). Fixes #804
* mattermost: Handle unthreaded messages (mattermost). Fixes #803
* mattermost: Use paging in initUser and UpdateUsers (mattermost)
* slack: Add lacking clean-up in Slack synchronisation (#811)
* slack: Disable user lookups on delete messages (slack) (#812)
# v1.14.3
## Bugfix
* irc: Fix deadlock on reconnect (irc). Closes #757
# v1.14.2
## Bugfix
* general: Update tengo vendor and load the stdlib. Fixes #789 (#792)
* rocketchat: Look up #channel too (rocketchat). Fix #773 (#775)
* slack: Ignore messagereplied and hidden messages (slack). Fixes #709 (#779)
* telegram: Handle nil message (telegram). Fixes #777
* irc: Use default nick if none specified (irc). Fixes #785
* irc: Return when not connected and drop a message (irc). Fixes #786
* irc: Revert fix for #722 (Support quits from irc correctly). Closes #781
## Contributors
This release couldn't exist without the following contributors:
@42wim, @Helcaraxan, @dajohi
# v1.14.1
## Bugfix
* slack: Fix crash double unlock (slack) (#771)
# v1.14.0
## Breaking
@ -19,6 +53,8 @@
## Enhancements
* general: Fail gracefully on incorrect human input. Fixes #739 (#740)
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
* matrix: Send notices on join/parts (matrix). Fixes #712 (#716)
## Bugfix
* general: Handle file upload/download only once for each message (#742)
@ -27,9 +63,9 @@
* irc: add support for (older) unrealircd versions. #708
* irc: Support quits from irc correctly. Fixes #722 (#724)
* matrix: Send username when uploading video/images (matrix). Fixes #715 (#717)
* matrix: Send notices on join/parts (matrix). Fixes #712 (#716)
* matrix: Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719)
* matrix: Trim <p> and </p> tags (matrix). Closes #686 (#753)
* slack: Hint at thread replies when messages are unthreaded (slack) (#684)
* slack: Fix race-condition in populateUser() (#767)
* xmpp: Do not send topic changes on connect (xmpp). Fixes #732 (#733)
* telegram: Fix regression in HTML handling (telegram). Closes #734
* discord: Do not relay any bot messages (discord) (#743)

View File

@ -1,5 +1,8 @@
#!/bin/bash
go version | grep go1.11 || exit
#!/usr/bin/env bash
set -u -e -x -o pipefail
go version | grep go1.12 || exit
VERSION=$(git describe --tags)
mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe

17
ci/lint.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${GOLANGCI_VERSION-}" ]]; then
# Retrieve the golangci-lint linter binary.
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
fi
# Run the linter.
golangci-lint run
if [[ "${GO111MODULE-off}" == "on" ]]; then
# If Go modules are active then check that dependencies are correctly maintained.
go mod tidy
go mod vendor
git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
fi

17
ci/test.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${REPORT_COVERAGE+cover}" ]]; then
# Retrieve and prepare CodeClimate's test coverage reporter.
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter before-build
fi
# Run all the tests with the race detector and generate coverage.
go test -v -race -coverprofile c.out ./...
if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then
# Upload test coverage to CodeClimate.
./cc-test-reporter after-build
fi

View File

@ -10,6 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/d5/tengo/script"
"github.com/d5/tengo/stdlib"
lru "github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
"github.com/sirupsen/logrus"
@ -211,23 +212,6 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels
}
// irc quit is for the whole bridge, isn't a per channel quit.
// channel is empty when we quit
if msg.Event == config.EventJoinLeave && getProtocol(msg) == "irc" && msg.Channel == "" {
// if we only have one channel on this irc bridge it's got to be the sending one.
// don't send it back
if dest.Account == msg.Account && len(dest.Channels) == 1 && dest.Protocol == "irc" {
return channels
}
for _, channel := range gw.Channels {
if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") &&
gw.validGatewayDest(msg) {
channels = append(channels, *channel)
}
}
return channels
}
// if source channel is in only, do nothing
for _, channel := range gw.Channels {
// lookup the channel from the message
@ -503,6 +487,7 @@ func modifyMessageTengo(filename string, msg *config.Message) error {
return err
}
s := script.New(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgAccount", msg.Account)

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/Jeffail/gabs v1.1.1 // indirect
github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329
github.com/bwmarrin/discordgo v0.19.0
github.com/d5/tengo v1.9.2
github.com/d5/tengo v1.20.0
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
github.com/fsnotify/fsnotify v1.4.7
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible

4
go.sum
View File

@ -15,8 +15,8 @@ github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVO
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/d5/tengo v1.9.2 h1:UE/X8PYl7bLS4Ww2zGeh91nq5PTnkhe8ncgNeA5PK7k=
github.com/d5/tengo v1.9.2/go.mod h1:gsbjo7lBXzBIWBd6NQp1lRKqqiDDANqBOyhW8rTlFsY=
github.com/d5/tengo v1.20.0 h1:lFmktzEGR6khlZu2MHUWJ5oDWS4l3jNRV/OhclZgcYc=
github.com/d5/tengo v1.20.0/go.mod h1:gsbjo7lBXzBIWBd6NQp1lRKqqiDDANqBOyhW8rTlFsY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -15,7 +15,7 @@ import (
)
var (
version = "1.14.0-rc2"
version = "1.14.4"
githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file")

View File

@ -27,7 +27,7 @@ UseTLS=false
#OPTIONAL (default false)
UseSASL=false
#Enable to not verify the certificate on your irc server. i
#Enable to not verify the certificate on your irc server.
#e.g. when using selfsigned certificates
#OPTIONAL (default false)
SkipTLSVerify=true
@ -1007,9 +1007,10 @@ ShowTopicChange=false
Server="https://yourrocketchatserver.domain.com:443"
#login/pass of your bot.
#login needs to be the login with email address! user@domain.com
#Use a dedicated user for this and not your own!
#REQUIRED (when not using webhooks)
Login="yourlogin"
Login="yourlogin@domain.com"
Password="yourpass"
#### Settings for webhook matterbridge.
@ -1050,6 +1051,8 @@ SkipTLSVerify=true
#Useful if username overrides for incoming webhooks isn't enabled on the
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
#from bridge to rocketchat will by default be prefixed by the RemoteNickFormat setting. i
#if you're using login/pass you can better enable because of this bug:
#https://github.com/RocketChat/Rocket.Chat/issues/7549
#OPTIONAL (default false)
PrefixMessagesWithNick=false

View File

@ -51,8 +51,9 @@ func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:go
if res == name {
return channel.Id
}
} else if channel.Name == name {
return channel.Id
}
}
}
return ""

View File

@ -132,14 +132,25 @@ func (m *MMClient) initUser() error {
return resp.Error
}
for _, team := range teams {
mmusers, resp := m.Client.GetUsersInTeam(team.Id, 0, 50000, "")
idx := 0
max := 200
usermap := make(map[string]*model.User)
mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
usermap := make(map[string]*model.User)
for _, user := range mmusers {
usermap[user.Id] = user
for len(mmusers) > 0 {
for _, user := range mmusers {
usermap[user.Id] = user
}
mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
time.Sleep(time.Millisecond * 200)
}
m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
t := &Team{Team: team, Users: usermap, Id: team.Id}

View File

@ -216,9 +216,14 @@ func (m *MMClient) WsReceiver() {
if msg.Post != nil {
if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
m.MessageChan <- msg
continue
}
}
continue
switch msg.Raw.Event {
case model.WEBSOCKET_EVENT_USER_ADDED, model.WEBSOCKET_EVENT_USER_REMOVED:
m.MessageChan <- msg
continue
}
}
var response model.WebSocketResponse

View File

@ -83,7 +83,7 @@ func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
post := &model.Post{Message: text}
post := &model.Post{Message: text, Id: postId}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error

View File

@ -2,6 +2,7 @@ package matterclient
import (
"errors"
"time"
"github.com/mattermost/mattermost-server/model"
)
@ -99,15 +100,25 @@ func (m *MMClient) GetUsers() map[string]*model.User {
}
func (m *MMClient) UpdateUsers() error {
mmusers, resp := m.Client.GetUsers(0, 50000, "")
idx := 0
max := 200
mmusers, resp := m.Client.GetUsers(idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
for len(mmusers) > 0 {
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock()
mmusers, resp = m.Client.GetUsers(idx, max, "")
time.Sleep(time.Millisecond * 300)
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
}
m.Unlock()
return nil
}

1
vendor/github.com/d5/tengo/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
dist/

23
vendor/github.com/d5/tengo/.goreleaser.yml generated vendored Normal file
View File

@ -0,0 +1,23 @@
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/tengo/main.go
goos:
- darwin
- linux
- windows
- env:
- CGO_ENABLED=0
main: ./cmd/tengomin/main.go
binary: tengomin
goos:
- darwin
- linux
- windows
archive:
files:
- none*
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc

17
vendor/github.com/d5/tengo/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,17 @@
language: go
go:
- 1.9
install:
- go get -u golang.org/x/lint/golint
script:
- make test
deploy:
- provider: script
skip_cleanup: true
script: curl -sL https://git.io/goreleaser | bash
on:
tags: true

14
vendor/github.com/d5/tengo/Makefile generated vendored Normal file
View File

@ -0,0 +1,14 @@
vet:
go vet ./...
generate:
go generate ./...
lint:
golint -set_exit_status ./...
test: generate vet lint
go test -race -cover ./...
fmt:
go fmt ./...

76
vendor/github.com/d5/tengo/README.md generated vendored Normal file
View File

@ -0,0 +1,76 @@
<p align="center">
<img src="https://raw.githubusercontent.com/d5/tengolang.com/master/logo_400.png" width="200" height="200">
</p>
# The Tengo Language
[![GoDoc](https://godoc.org/github.com/d5/tengo?status.svg)](https://godoc.org/github.com/d5/tengo/script)
[![Go Report Card](https://goreportcard.com/badge/github.com/d5/tengo)](https://goreportcard.com/report/github.com/d5/tengo)
[![Build Status](https://travis-ci.org/d5/tengo.svg?branch=master)](https://travis-ci.org/d5/tengo)
**Tengo is a small, dynamic, fast, secure script language for Go.**
Tengo is **[fast](#benchmark)** and secure because it's compiled/executed as bytecode on stack-based VM that's written in native Go.
```golang
/* The Tengo Language */
fmt := import("fmt")
each := func(seq, fn) {
for x in seq { fn(x) }
}
sum := func(init, seq) {
each(seq, func(x) { init += x })
return init
}
fmt.println(sum(0, [1, 2, 3])) // "6"
fmt.println(sum("", [1, 2, 3])) // "123"
```
> Run this code in the [Playground](https://tengolang.com/?s=0c8d5d0d88f2795a7093d7f35ae12c3afa17bea3)
## Features
- Simple and highly readable [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- Dynamic typing with type coercion
- Higher-order functions and closures
- Immutable values
- Garbage collection
- [Securely Embeddable](https://github.com/d5/tengo/blob/master/docs/interoperability.md) and [Extensible](https://github.com/d5/tengo/blob/master/docs/objects.md)
- Compiler/runtime written in native Go _(no external deps or cgo)_
- Executable as a [standalone](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) language / REPL
- Use cases: rules engine, [state machine](https://github.com/d5/go-fsm), [gaming](https://github.com/d5/pbr), data pipeline, [transpiler](https://github.com/d5/tengo2lua)
## Benchmark
| | fib(35) | fibt(35) | Type |
| :--- | ---: | ---: | :---: |
| Go | `48ms` | `3ms` | Go (native) |
| [**Tengo**](https://github.com/d5/tengo) | `2,349ms` | `5ms` | VM on Go |
| Lua | `1,416ms` | `3ms` | Lua (native) |
| [go-lua](https://github.com/Shopify/go-lua) | `4,402ms` | `5ms` | Lua VM on Go |
| [GopherLua](https://github.com/yuin/gopher-lua) | `4,023ms` | `5ms` | Lua VM on Go |
| Python | `2,588ms` | `26ms` | Python (native) |
| [starlark-go](https://github.com/google/starlark-go) | `11,126ms` | `6ms` | Python-like Interpreter on Go |
| [gpython](https://github.com/go-python/gpython) | `15,035ms` | `4ms` | Python Interpreter on Go |
| [goja](https://github.com/dop251/goja) | `5,089ms` | `5ms` | JS VM on Go |
| [otto](https://github.com/robertkrimen/otto) | `68,377ms` | `11ms` | JS Interpreter on Go |
| [Anko](https://github.com/mattn/anko) | `92,579ms` | `18ms` | Interpreter on Go |
_* [fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo): Fibonacci(35)_
_* [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo): [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of Fibonacci(35)_
_* **Go** does not read the source code from file, while all other cases do_
_* See [here](https://github.com/d5/tengobench) for commands/codes used_
## References
- [Language Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md)
- [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md)
- [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) and [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md)
- [Builtin Functions](https://github.com/d5/tengo/blob/master/docs/builtins.md)
- [Interoperability](https://github.com/d5/tengo/blob/master/docs/interoperability.md)
- [Tengo CLI](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md)
- [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md)

View File

@ -17,32 +17,6 @@ type Bytecode struct {
Constants []objects.Object
}
// Decode reads Bytecode data from the reader.
func (b *Bytecode) Decode(r io.Reader) error {
dec := gob.NewDecoder(r)
if err := dec.Decode(&b.FileSet); err != nil {
return err
}
// TODO: files in b.FileSet.File does not have their 'set' field properly set to b.FileSet
// as it's private field and not serialized by gob encoder/decoder.
if err := dec.Decode(&b.MainFunction); err != nil {
return err
}
if err := dec.Decode(&b.Constants); err != nil {
return err
}
// replace Bool and Undefined with known value
for i, v := range b.Constants {
b.Constants[i] = cleanupObjects(v)
}
return nil
}
// Encode writes Bytecode data to the writer.
func (b *Bytecode) Encode(w io.Writer) error {
enc := gob.NewEncoder(w)
@ -59,6 +33,17 @@ func (b *Bytecode) Encode(w io.Writer) error {
return enc.Encode(b.Constants)
}
// CountObjects returns the number of objects found in Constants.
func (b *Bytecode) CountObjects() int {
n := 0
for _, c := range b.Constants {
n += objects.CountObjects(c)
}
return n
}
// FormatInstructions returns human readable string representations of
// compiled instructions.
func (b *Bytecode) FormatInstructions() []string {
@ -83,51 +68,22 @@ func (b *Bytecode) FormatConstants() (output []string) {
return
}
func cleanupObjects(o objects.Object) objects.Object {
switch o := o.(type) {
case *objects.Bool:
if o.IsFalsy() {
return objects.FalseValue
}
return objects.TrueValue
case *objects.Undefined:
return objects.UndefinedValue
case *objects.Array:
for i, v := range o.Value {
o.Value[i] = cleanupObjects(v)
}
case *objects.Map:
for k, v := range o.Value {
o.Value[k] = cleanupObjects(v)
}
}
return o
}
func init() {
gob.Register(&source.FileSet{})
gob.Register(&source.File{})
gob.Register(&objects.Array{})
gob.Register(&objects.ArrayIterator{})
gob.Register(&objects.Bool{})
gob.Register(&objects.Break{})
gob.Register(&objects.BuiltinFunction{})
gob.Register(&objects.Bytes{})
gob.Register(&objects.Char{})
gob.Register(&objects.Closure{})
gob.Register(&objects.CompiledFunction{})
gob.Register(&objects.Continue{})
gob.Register(&objects.Error{})
gob.Register(&objects.Float{})
gob.Register(&objects.ImmutableArray{})
gob.Register(&objects.ImmutableMap{})
gob.Register(&objects.Int{})
gob.Register(&objects.Map{})
gob.Register(&objects.MapIterator{})
gob.Register(&objects.ReturnValue{})
gob.Register(&objects.String{})
gob.Register(&objects.StringIterator{})
gob.Register(&objects.Time{})
gob.Register(&objects.Undefined{})
gob.Register(&objects.UserFunction{})

97
vendor/github.com/d5/tengo/compiler/bytecode_decode.go generated vendored Normal file
View File

@ -0,0 +1,97 @@
package compiler
import (
"encoding/gob"
"fmt"
"io"
"github.com/d5/tengo/objects"
)
// Decode reads Bytecode data from the reader.
func (b *Bytecode) Decode(r io.Reader, modules *objects.ModuleMap) error {
if modules == nil {
modules = objects.NewModuleMap()
}
dec := gob.NewDecoder(r)
if err := dec.Decode(&b.FileSet); err != nil {
return err
}
// TODO: files in b.FileSet.File does not have their 'set' field properly set to b.FileSet
// as it's private field and not serialized by gob encoder/decoder.
if err := dec.Decode(&b.MainFunction); err != nil {
return err
}
if err := dec.Decode(&b.Constants); err != nil {
return err
}
for i, v := range b.Constants {
fv, err := fixDecoded(v, modules)
if err != nil {
return err
}
b.Constants[i] = fv
}
return nil
}
func fixDecoded(o objects.Object, modules *objects.ModuleMap) (objects.Object, error) {
switch o := o.(type) {
case *objects.Bool:
if o.IsFalsy() {
return objects.FalseValue, nil
}
return objects.TrueValue, nil
case *objects.Undefined:
return objects.UndefinedValue, nil
case *objects.Array:
for i, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[i] = fv
}
case *objects.ImmutableArray:
for i, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[i] = fv
}
case *objects.Map:
for k, v := range o.Value {
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[k] = fv
}
case *objects.ImmutableMap:
modName := moduleName(o)
if mod := modules.GetBuiltinModule(modName); mod != nil {
return mod.AsImmutableMap(modName), nil
}
for k, v := range o.Value {
// encoding of user function not supported
if _, isUserFunction := v.(*objects.UserFunction); isUserFunction {
return nil, fmt.Errorf("user function not decodable")
}
fv, err := fixDecoded(v, modules)
if err != nil {
return nil, err
}
o.Value[k] = fv
}
}
return o, nil
}

View File

@ -0,0 +1,129 @@
package compiler
import (
"fmt"
"github.com/d5/tengo/objects"
)
// RemoveDuplicates finds and remove the duplicate values in Constants.
// Note this function mutates Bytecode.
func (b *Bytecode) RemoveDuplicates() {
var deduped []objects.Object
indexMap := make(map[int]int) // mapping from old constant index to new index
ints := make(map[int64]int)
strings := make(map[string]int)
floats := make(map[float64]int)
chars := make(map[rune]int)
immutableMaps := make(map[string]int) // for modules
for curIdx, c := range b.Constants {
switch c := c.(type) {
case *objects.CompiledFunction:
// add to deduped list
indexMap[curIdx] = len(deduped)
deduped = append(deduped, c)
case *objects.ImmutableMap:
modName := moduleName(c)
newIdx, ok := immutableMaps[modName]
if modName != "" && ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
immutableMaps[modName] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Int:
if newIdx, ok := ints[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
ints[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.String:
if newIdx, ok := strings[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
strings[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Float:
if newIdx, ok := floats[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
floats[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
case *objects.Char:
if newIdx, ok := chars[c.Value]; ok {
indexMap[curIdx] = newIdx
} else {
newIdx = len(deduped)
chars[c.Value] = newIdx
indexMap[curIdx] = newIdx
deduped = append(deduped, c)
}
default:
panic(fmt.Errorf("unsupported top-level constant type: %s", c.TypeName()))
}
}
// replace with de-duplicated constants
b.Constants = deduped
// update CONST instructions with new indexes
// main function
updateConstIndexes(b.MainFunction.Instructions, indexMap)
// other compiled functions in constants
for _, c := range b.Constants {
switch c := c.(type) {
case *objects.CompiledFunction:
updateConstIndexes(c.Instructions, indexMap)
}
}
}
func updateConstIndexes(insts []byte, indexMap map[int]int) {
i := 0
for i < len(insts) {
op := insts[i]
numOperands := OpcodeOperands[op]
_, read := ReadOperands(numOperands, insts[i+1:])
switch op {
case OpConstant:
curIdx := int(insts[i+2]) | int(insts[i+1])<<8
newIdx, ok := indexMap[curIdx]
if !ok {
panic(fmt.Errorf("constant index not found: %d", curIdx))
}
copy(insts[i:], MakeInstruction(op, newIdx))
case OpClosure:
curIdx := int(insts[i+2]) | int(insts[i+1])<<8
numFree := int(insts[i+3])
newIdx, ok := indexMap[curIdx]
if !ok {
panic(fmt.Errorf("constant index not found: %d", curIdx))
}
copy(insts[i:], MakeInstruction(op, newIdx, numFree))
}
i += 1 + read
}
}
func moduleName(mod *objects.ImmutableMap) string {
if modName, ok := mod.Value["__module_name__"].(*objects.String); ok {
return modName.Value
}
return ""
}

View File

@ -5,8 +5,7 @@ import "github.com/d5/tengo/compiler/source"
// CompilationScope represents a compiled instructions
// and the last two instructions that were emitted.
type CompilationScope struct {
instructions []byte
lastInstructions [2]EmittedInstruction
symbolInit map[string]bool
sourceMap map[int]source.Pos
instructions []byte
symbolInit map[string]bool
sourceMap map[int]source.Pos
}

View File

@ -3,27 +3,30 @@ package compiler
import (
"fmt"
"io"
"io/ioutil"
"path/filepath"
"reflect"
"strings"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/token"
"github.com/d5/tengo/objects"
"github.com/d5/tengo/stdlib"
)
// Compiler compiles the AST into a bytecode.
type Compiler struct {
file *source.File
parent *Compiler
moduleName string
modulePath string
constants []objects.Object
symbolTable *SymbolTable
scopes []CompilationScope
scopeIndex int
moduleLoader ModuleLoader
builtinModules map[string]bool
modules *objects.ModuleMap
compiledModules map[string]*objects.CompiledFunction
allowFileImport bool
loops []*Loop
loopIndex int
trace io.Writer
@ -31,12 +34,7 @@ type Compiler struct {
}
// NewCompiler creates a Compiler.
// User can optionally provide the symbol table if one wants to add or remove
// some global- or builtin- scope symbols. If not (nil), Compile will create
// a new symbol table and use the default builtin functions. Likewise, standard
// modules can be explicitly provided if user wants to add or remove some modules.
// By default, Compile will use all the standard modules otherwise.
func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []objects.Object, builtinModules map[string]bool, trace io.Writer) *Compiler {
func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []objects.Object, modules *objects.ModuleMap, trace io.Writer) *Compiler {
mainScope := CompilationScope{
symbolInit: make(map[string]bool),
sourceMap: make(map[int]source.Pos),
@ -45,18 +43,16 @@ func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []object
// symbol table
if symbolTable == nil {
symbolTable = NewSymbolTable()
}
for idx, fn := range objects.Builtins {
symbolTable.DefineBuiltin(idx, fn.Name)
}
// add builtin functions to the symbol table
for idx, fn := range objects.Builtins {
symbolTable.DefineBuiltin(idx, fn.Name)
}
// builtin modules
if builtinModules == nil {
builtinModules = make(map[string]bool)
for name := range stdlib.Modules {
builtinModules[name] = true
}
if modules == nil {
modules = objects.NewModuleMap()
}
return &Compiler{
@ -67,7 +63,7 @@ func NewCompiler(file *source.File, symbolTable *SymbolTable, constants []object
scopeIndex: 0,
loopIndex: -1,
trace: trace,
builtinModules: builtinModules,
modules: modules,
compiledModules: make(map[string]*objects.CompiledFunction),
}
}
@ -123,7 +119,7 @@ func (c *Compiler) Compile(node ast.Node) error {
return err
}
c.emit(node, OpGreaterThan)
c.emit(node, OpBinaryOp, int(token.Greater))
return nil
} else if node.Token == token.LessEq {
@ -134,7 +130,7 @@ func (c *Compiler) Compile(node ast.Node) error {
return err
}
c.emit(node, OpGreaterThanEqual)
c.emit(node, OpBinaryOp, int(token.GreaterEq))
return nil
}
@ -148,35 +144,35 @@ func (c *Compiler) Compile(node ast.Node) error {
switch node.Token {
case token.Add:
c.emit(node, OpAdd)
c.emit(node, OpBinaryOp, int(token.Add))
case token.Sub:
c.emit(node, OpSub)
c.emit(node, OpBinaryOp, int(token.Sub))
case token.Mul:
c.emit(node, OpMul)
c.emit(node, OpBinaryOp, int(token.Mul))
case token.Quo:
c.emit(node, OpDiv)
c.emit(node, OpBinaryOp, int(token.Quo))
case token.Rem:
c.emit(node, OpRem)
c.emit(node, OpBinaryOp, int(token.Rem))
case token.Greater:
c.emit(node, OpGreaterThan)
c.emit(node, OpBinaryOp, int(token.Greater))
case token.GreaterEq:
c.emit(node, OpGreaterThanEqual)
c.emit(node, OpBinaryOp, int(token.GreaterEq))
case token.Equal:
c.emit(node, OpEqual)
case token.NotEqual:
c.emit(node, OpNotEqual)
case token.And:
c.emit(node, OpBAnd)
c.emit(node, OpBinaryOp, int(token.And))
case token.Or:
c.emit(node, OpBOr)
c.emit(node, OpBinaryOp, int(token.Or))
case token.Xor:
c.emit(node, OpBXor)
c.emit(node, OpBinaryOp, int(token.Xor))
case token.AndNot:
c.emit(node, OpBAndNot)
c.emit(node, OpBinaryOp, int(token.AndNot))
case token.Shl:
c.emit(node, OpBShiftLeft)
c.emit(node, OpBinaryOp, int(token.Shl))
case token.Shr:
c.emit(node, OpBShiftRight)
c.emit(node, OpBinaryOp, int(token.Shr))
default:
return c.errorf(node, "invalid binary operator: %s", node.Token.String())
}
@ -195,6 +191,10 @@ func (c *Compiler) Compile(node ast.Node) error {
}
case *ast.StringLit:
if len(node.Value) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.Value}))
case *ast.CharLit:
@ -292,6 +292,15 @@ func (c *Compiler) Compile(node ast.Node) error {
}
case *ast.BlockStmt:
if len(node.Stmts) == 0 {
return nil
}
c.symbolTable = c.symbolTable.Fork(true)
defer func() {
c.symbolTable = c.symbolTable.Parent(false)
}()
for _, stmt := range node.Stmts {
if err := c.Compile(stmt); err != nil {
return err
@ -332,6 +341,9 @@ func (c *Compiler) Compile(node ast.Node) error {
case *ast.MapLit:
for _, elt := range node.Elements {
// key
if len(elt.Key) > tengo.MaxStringLen {
return c.error(node, objects.ErrStringLimit)
}
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: elt.Key}))
// value
@ -401,10 +413,8 @@ func (c *Compiler) Compile(node ast.Node) error {
return err
}
// add OpReturn if function returns nothing
if !c.lastInstructionIs(OpReturnValue) && !c.lastInstructionIs(OpReturn) {
c.emit(node, OpReturn)
}
// code optimization
c.optimizeFunc(node)
freeSymbols := c.symbolTable.FreeSymbols()
numLocals := c.symbolTable.MaxSymbols()
@ -457,9 +467,9 @@ func (c *Compiler) Compile(node ast.Node) error {
s.LocalAssigned = true
}
c.emit(node, OpGetLocal, s.Index)
c.emit(node, OpGetLocalPtr, s.Index)
case ScopeFree:
c.emit(node, OpGetFree, s.Index)
c.emit(node, OpGetFreePtr, s.Index)
}
}
@ -483,13 +493,13 @@ func (c *Compiler) Compile(node ast.Node) error {
}
if node.Result == nil {
c.emit(node, OpReturn)
c.emit(node, OpReturn, 0)
} else {
if err := c.Compile(node.Result); err != nil {
return err
}
c.emit(node, OpReturnValue)
c.emit(node, OpReturn, 1)
}
case *ast.CallExpr:
@ -506,17 +516,57 @@ func (c *Compiler) Compile(node ast.Node) error {
c.emit(node, OpCall, len(node.Args))
case *ast.ImportExpr:
if c.builtinModules[node.ModuleName] {
c.emit(node, OpConstant, c.addConstant(&objects.String{Value: node.ModuleName}))
c.emit(node, OpGetBuiltinModule)
} else {
userMod, err := c.compileModule(node)
if node.ModuleName == "" {
return c.errorf(node, "empty module name")
}
if mod := c.modules.Get(node.ModuleName); mod != nil {
v, err := mod.Import(node.ModuleName)
if err != nil {
return err
}
c.emit(node, OpConstant, c.addConstant(userMod))
switch v := v.(type) {
case []byte: // module written in Tengo
compiled, err := c.compileModule(node, node.ModuleName, node.ModuleName, v)
if err != nil {
return err
}
c.emit(node, OpConstant, c.addConstant(compiled))
c.emit(node, OpCall, 0)
case objects.Object: // builtin module
c.emit(node, OpConstant, c.addConstant(v))
default:
panic(fmt.Errorf("invalid import value type: %T", v))
}
} else if c.allowFileImport {
moduleName := node.ModuleName
if !strings.HasSuffix(moduleName, ".tengo") {
moduleName += ".tengo"
}
modulePath, err := filepath.Abs(moduleName)
if err != nil {
return c.errorf(node, "module file path error: %s", err.Error())
}
if err := c.checkCyclicImports(node, modulePath); err != nil {
return err
}
moduleSrc, err := ioutil.ReadFile(moduleName)
if err != nil {
return c.errorf(node, "module file read error: %s", err.Error())
}
compiled, err := c.compileModule(node, moduleName, modulePath, moduleSrc)
if err != nil {
return err
}
c.emit(node, OpConstant, c.addConstant(compiled))
c.emit(node, OpCall, 0)
} else {
return c.errorf(node, "module '%s' not found", node.ModuleName)
}
case *ast.ExportStmt:
@ -535,7 +585,7 @@ func (c *Compiler) Compile(node ast.Node) error {
}
c.emit(node, OpImmutable)
c.emit(node, OpReturnValue)
c.emit(node, OpReturn, 1)
case *ast.ErrorExpr:
if err := c.Compile(node.Expr); err != nil {
@ -594,22 +644,28 @@ func (c *Compiler) Bytecode() *Bytecode {
}
}
// SetModuleLoader sets or replaces the current module loader.
// Note that the module loader is used for user modules,
// not for the standard modules.
func (c *Compiler) SetModuleLoader(moduleLoader ModuleLoader) {
c.moduleLoader = moduleLoader
// EnableFileImport enables or disables module loading from local files.
// Local file modules are disabled by default.
func (c *Compiler) EnableFileImport(enable bool) {
c.allowFileImport = enable
}
func (c *Compiler) fork(file *source.File, moduleName string, symbolTable *SymbolTable) *Compiler {
child := NewCompiler(file, symbolTable, nil, c.builtinModules, c.trace)
child.moduleName = moduleName // name of the module to compile
child.parent = c // parent to set to current compiler
child.moduleLoader = c.moduleLoader // share module loader
func (c *Compiler) fork(file *source.File, modulePath string, symbolTable *SymbolTable) *Compiler {
child := NewCompiler(file, symbolTable, nil, c.modules, c.trace)
child.modulePath = modulePath // module file path
child.parent = c // parent to set to current compiler
return child
}
func (c *Compiler) error(node ast.Node, err error) error {
return &Error{
fileSet: c.file.Set(),
node: node,
error: err,
}
}
func (c *Compiler) errorf(node ast.Node, format string, args ...interface{}) error {
return &Error{
fileSet: c.file.Set(),
@ -641,33 +697,6 @@ func (c *Compiler) addInstruction(b []byte) int {
return posNewIns
}
func (c *Compiler) setLastInstruction(op Opcode, pos int) {
c.scopes[c.scopeIndex].lastInstructions[1] = c.scopes[c.scopeIndex].lastInstructions[0]
c.scopes[c.scopeIndex].lastInstructions[0].Opcode = op
c.scopes[c.scopeIndex].lastInstructions[0].Position = pos
}
func (c *Compiler) lastInstructionIs(op Opcode) bool {
if len(c.currentInstructions()) == 0 {
return false
}
return c.scopes[c.scopeIndex].lastInstructions[0].Opcode == op
}
func (c *Compiler) removeLastInstruction() {
lastPos := c.scopes[c.scopeIndex].lastInstructions[0].Position
if c.trace != nil {
c.printTrace(fmt.Sprintf("DELET %s",
FormatInstructions(c.scopes[c.scopeIndex].instructions[lastPos:], lastPos)[0]))
}
c.scopes[c.scopeIndex].instructions = c.currentInstructions()[:lastPos]
c.scopes[c.scopeIndex].lastInstructions[0] = c.scopes[c.scopeIndex].lastInstructions[1]
}
func (c *Compiler) replaceInstruction(pos int, inst []byte) {
copy(c.currentInstructions()[pos:], inst)
@ -684,6 +713,92 @@ func (c *Compiler) changeOperand(opPos int, operand ...int) {
c.replaceInstruction(opPos, inst)
}
// optimizeFunc performs some code-level optimization for the current function instructions
// it removes unreachable (dead code) instructions and adds "returns" instruction if needed.
func (c *Compiler) optimizeFunc(node ast.Node) {
// any instructions between RETURN and the function end
// or instructions between RETURN and jump target position
// are considered as unreachable.
// pass 1. identify all jump destinations
dsts := make(map[int]bool)
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
dsts[operands[0]] = true
}
return true
})
var newInsts []byte
// pass 2. eliminate dead code
posMap := make(map[int]int) // old position to new position
var dstIdx int
var deadCode bool
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch {
case opcode == OpReturn:
if deadCode {
return true
}
deadCode = true
case dsts[pos]:
dstIdx++
deadCode = false
case deadCode:
return true
}
posMap[pos] = len(newInsts)
newInsts = append(newInsts, MakeInstruction(opcode, operands...)...)
return true
})
// pass 3. update jump positions
var lastOp Opcode
var appendReturn bool
endPos := len(c.scopes[c.scopeIndex].instructions)
iterateInstructions(newInsts, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
newDst, ok := posMap[operands[0]]
if ok {
copy(newInsts[pos:], MakeInstruction(opcode, newDst))
} else if endPos == operands[0] {
// there's a jump instruction that jumps to the end of function
// compiler should append "return".
appendReturn = true
} else {
panic(fmt.Errorf("invalid jump position: %d", newDst))
}
}
lastOp = opcode
return true
})
if lastOp != OpReturn {
appendReturn = true
}
// pass 4. update source map
newSourceMap := make(map[int]source.Pos)
for pos, srcPos := range c.scopes[c.scopeIndex].sourceMap {
newPos, ok := posMap[pos]
if ok {
newSourceMap[newPos] = srcPos
}
}
c.scopes[c.scopeIndex].instructions = newInsts
c.scopes[c.scopeIndex].sourceMap = newSourceMap
// append "return"
if appendReturn {
c.emit(node, OpReturn, 0)
}
}
func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int {
filePos := source.NoPos
if node != nil {
@ -693,7 +808,6 @@ func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int {
inst := MakeInstruction(opcode, operands...)
pos := c.addInstruction(inst)
c.scopes[c.scopeIndex].sourceMap[pos] = filePos
c.setLastInstruction(opcode, pos)
if c.trace != nil {
c.printTrace(fmt.Sprintf("EMIT %s",

View File

@ -51,27 +51,27 @@ func (c *Compiler) compileAssign(node ast.Node, lhs, rhs []ast.Expr, op token.To
switch op {
case token.AddAssign:
c.emit(node, OpAdd)
c.emit(node, OpBinaryOp, int(token.Add))
case token.SubAssign:
c.emit(node, OpSub)
c.emit(node, OpBinaryOp, int(token.Sub))
case token.MulAssign:
c.emit(node, OpMul)
c.emit(node, OpBinaryOp, int(token.Mul))
case token.QuoAssign:
c.emit(node, OpDiv)
c.emit(node, OpBinaryOp, int(token.Quo))
case token.RemAssign:
c.emit(node, OpRem)
c.emit(node, OpBinaryOp, int(token.Rem))
case token.AndAssign:
c.emit(node, OpBAnd)
c.emit(node, OpBinaryOp, int(token.And))
case token.OrAssign:
c.emit(node, OpBOr)
c.emit(node, OpBinaryOp, int(token.Or))
case token.AndNotAssign:
c.emit(node, OpBAndNot)
c.emit(node, OpBinaryOp, int(token.AndNot))
case token.XorAssign:
c.emit(node, OpBXor)
c.emit(node, OpBinaryOp, int(token.Xor))
case token.ShlAssign:
c.emit(node, OpBShiftLeft)
c.emit(node, OpBinaryOp, int(token.Shl))
case token.ShrAssign:
c.emit(node, OpBShiftRight)
c.emit(node, OpBinaryOp, int(token.Shr))
}
// compile selector expressions (right to left)

View File

@ -1,72 +1,31 @@
package compiler
import (
"io/ioutil"
"strings"
"github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/parser"
"github.com/d5/tengo/objects"
)
func (c *Compiler) compileModule(expr *ast.ImportExpr) (*objects.CompiledFunction, error) {
compiledModule, exists := c.loadCompiledModule(expr.ModuleName)
if exists {
return compiledModule, nil
}
moduleName := expr.ModuleName
// read module source from loader
var moduleSrc []byte
if c.moduleLoader == nil {
// default loader: read from local file
if !strings.HasSuffix(moduleName, ".tengo") {
moduleName += ".tengo"
}
if err := c.checkCyclicImports(expr, moduleName); err != nil {
return nil, err
}
var err error
moduleSrc, err = ioutil.ReadFile(moduleName)
if err != nil {
return nil, c.errorf(expr, "module file read error: %s", err.Error())
}
} else {
if err := c.checkCyclicImports(expr, moduleName); err != nil {
return nil, err
}
var err error
moduleSrc, err = c.moduleLoader(moduleName)
if err != nil {
return nil, err
}
}
compiledModule, err := c.doCompileModule(moduleName, moduleSrc)
if err != nil {
return nil, err
}
c.storeCompiledModule(moduleName, compiledModule)
return compiledModule, nil
}
func (c *Compiler) checkCyclicImports(node ast.Node, moduleName string) error {
if c.moduleName == moduleName {
return c.errorf(node, "cyclic module import: %s", moduleName)
func (c *Compiler) checkCyclicImports(node ast.Node, modulePath string) error {
if c.modulePath == modulePath {
return c.errorf(node, "cyclic module import: %s", modulePath)
} else if c.parent != nil {
return c.parent.checkCyclicImports(node, moduleName)
return c.parent.checkCyclicImports(node, modulePath)
}
return nil
}
func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.CompiledFunction, error) {
func (c *Compiler) compileModule(node ast.Node, moduleName, modulePath string, src []byte) (*objects.CompiledFunction, error) {
if err := c.checkCyclicImports(node, modulePath); err != nil {
return nil, err
}
compiledModule, exists := c.loadCompiledModule(modulePath)
if exists {
return compiledModule, nil
}
modFile := c.file.Set().AddFile(moduleName, -1, len(src))
p := parser.NewParser(modFile, src, nil)
file, err := p.ParseFile()
@ -77,47 +36,44 @@ func (c *Compiler) doCompileModule(moduleName string, src []byte) (*objects.Comp
symbolTable := NewSymbolTable()
// inherit builtin functions
for idx, fn := range objects.Builtins {
s, _, ok := c.symbolTable.Resolve(fn.Name)
if ok && s.Scope == ScopeBuiltin {
symbolTable.DefineBuiltin(idx, fn.Name)
}
for _, sym := range c.symbolTable.BuiltinSymbols() {
symbolTable.DefineBuiltin(sym.Index, sym.Name)
}
// no global scope for the module
symbolTable = symbolTable.Fork(false)
// compile module
moduleCompiler := c.fork(modFile, moduleName, symbolTable)
moduleCompiler := c.fork(modFile, modulePath, symbolTable)
if err := moduleCompiler.Compile(file); err != nil {
return nil, err
}
// add OpReturn (== export undefined) if export is missing
if !moduleCompiler.lastInstructionIs(OpReturnValue) {
moduleCompiler.emit(nil, OpReturn)
}
// code optimization
moduleCompiler.optimizeFunc(node)
compiledFunc := moduleCompiler.Bytecode().MainFunction
compiledFunc.NumLocals = symbolTable.MaxSymbols()
c.storeCompiledModule(modulePath, compiledFunc)
return compiledFunc, nil
}
func (c *Compiler) loadCompiledModule(moduleName string) (mod *objects.CompiledFunction, ok bool) {
func (c *Compiler) loadCompiledModule(modulePath string) (mod *objects.CompiledFunction, ok bool) {
if c.parent != nil {
return c.parent.loadCompiledModule(moduleName)
return c.parent.loadCompiledModule(modulePath)
}
mod, ok = c.compiledModules[moduleName]
mod, ok = c.compiledModules[modulePath]
return
}
func (c *Compiler) storeCompiledModule(moduleName string, module *objects.CompiledFunction) {
func (c *Compiler) storeCompiledModule(modulePath string, module *objects.CompiledFunction) {
if c.parent != nil {
c.parent.storeCompiledModule(moduleName, module)
c.parent.storeCompiledModule(modulePath, module)
}
c.compiledModules[moduleName] = module
c.compiledModules[modulePath] = module
}

View File

@ -57,3 +57,16 @@ func FormatInstructions(b []byte, posOffset int) []string {
return out
}
func iterateInstructions(b []byte, fn func(pos int, opcode Opcode, operands []int) bool) {
for i := 0; i < len(b); i++ {
numOperands := OpcodeOperands[Opcode(b[i])]
operands, read := ReadOperands(numOperands, b[i+1:])
if !fn(i, b[i], operands) {
break
}
i += read
}
}

View File

@ -5,173 +5,137 @@ type Opcode = byte
// List of opcodes
const (
OpConstant Opcode = iota // Load constant
OpAdd // Add
OpSub // Sub
OpMul // Multiply
OpDiv // Divide
OpRem // Remainder
OpBAnd // bitwise AND
OpBOr // bitwise OR
OpBXor // bitwise XOR
OpBShiftLeft // bitwise shift left
OpBShiftRight // bitwise shift right
OpBAndNot // bitwise AND NOT
OpBComplement // bitwise complement
OpPop // Pop
OpTrue // Push true
OpFalse // Push false
OpEqual // Equal ==
OpNotEqual // Not equal !=
OpGreaterThan // Greater than >=
OpGreaterThanEqual // Greater than or equal to >=
OpMinus // Minus -
OpLNot // Logical not !
OpJumpFalsy // Jump if falsy
OpAndJump // Logical AND jump
OpOrJump // Logical OR jump
OpJump // Jump
OpNull // Push null
OpArray // Array object
OpMap // Map object
OpError // Error object
OpImmutable // Immutable object
OpIndex // Index operation
OpSliceIndex // Slice operation
OpCall // Call function
OpReturn // Return
OpReturnValue // Return value
OpGetGlobal // Get global variable
OpSetGlobal // Set global variable
OpSetSelGlobal // Set global variable using selectors
OpGetLocal // Get local variable
OpSetLocal // Set local variable
OpDefineLocal // Define local variable
OpSetSelLocal // Set local variable using selectors
OpGetFree // Get free variables
OpSetFree // Set free variables
OpSetSelFree // Set free variables using selectors
OpGetBuiltin // Get builtin function
OpGetBuiltinModule // Get builtin module
OpClosure // Push closure
OpIteratorInit // Iterator init
OpIteratorNext // Iterator next
OpIteratorKey // Iterator key
OpIteratorValue // Iterator value
OpConstant Opcode = iota // Load constant
OpBComplement // bitwise complement
OpPop // Pop
OpTrue // Push true
OpFalse // Push false
OpEqual // Equal ==
OpNotEqual // Not equal !=
OpMinus // Minus -
OpLNot // Logical not !
OpJumpFalsy // Jump if falsy
OpAndJump // Logical AND jump
OpOrJump // Logical OR jump
OpJump // Jump
OpNull // Push null
OpArray // Array object
OpMap // Map object
OpError // Error object
OpImmutable // Immutable object
OpIndex // Index operation
OpSliceIndex // Slice operation
OpCall // Call function
OpReturn // Return
OpGetGlobal // Get global variable
OpSetGlobal // Set global variable
OpSetSelGlobal // Set global variable using selectors
OpGetLocal // Get local variable
OpSetLocal // Set local variable
OpDefineLocal // Define local variable
OpSetSelLocal // Set local variable using selectors
OpGetFreePtr // Get free variable pointer object
OpGetFree // Get free variables
OpSetFree // Set free variables
OpGetLocalPtr // Get local variable as a pointer
OpSetSelFree // Set free variables using selectors
OpGetBuiltin // Get builtin function
OpClosure // Push closure
OpIteratorInit // Iterator init
OpIteratorNext // Iterator next
OpIteratorKey // Iterator key
OpIteratorValue // Iterator value
OpBinaryOp // Binary Operation
)
// OpcodeNames is opcode names.
var OpcodeNames = [...]string{
OpConstant: "CONST",
OpPop: "POP",
OpTrue: "TRUE",
OpFalse: "FALSE",
OpAdd: "ADD",
OpSub: "SUB",
OpMul: "MUL",
OpDiv: "DIV",
OpRem: "REM",
OpBAnd: "AND",
OpBOr: "OR",
OpBXor: "XOR",
OpBAndNot: "ANDN",
OpBShiftLeft: "SHL",
OpBShiftRight: "SHR",
OpBComplement: "NEG",
OpEqual: "EQL",
OpNotEqual: "NEQ",
OpGreaterThan: "GTR",
OpGreaterThanEqual: "GEQ",
OpMinus: "NEG",
OpLNot: "NOT",
OpJumpFalsy: "JMPF",
OpAndJump: "ANDJMP",
OpOrJump: "ORJMP",
OpJump: "JMP",
OpNull: "NULL",
OpGetGlobal: "GETG",
OpSetGlobal: "SETG",
OpSetSelGlobal: "SETSG",
OpArray: "ARR",
OpMap: "MAP",
OpError: "ERROR",
OpImmutable: "IMMUT",
OpIndex: "INDEX",
OpSliceIndex: "SLICE",
OpCall: "CALL",
OpReturn: "RET",
OpReturnValue: "RETVAL",
OpGetLocal: "GETL",
OpSetLocal: "SETL",
OpDefineLocal: "DEFL",
OpSetSelLocal: "SETSL",
OpGetBuiltin: "BUILTIN",
OpGetBuiltinModule: "BLTMOD",
OpClosure: "CLOSURE",
OpGetFree: "GETF",
OpSetFree: "SETF",
OpSetSelFree: "SETSF",
OpIteratorInit: "ITER",
OpIteratorNext: "ITNXT",
OpIteratorKey: "ITKEY",
OpIteratorValue: "ITVAL",
OpConstant: "CONST",
OpPop: "POP",
OpTrue: "TRUE",
OpFalse: "FALSE",
OpBComplement: "NEG",
OpEqual: "EQL",
OpNotEqual: "NEQ",
OpMinus: "NEG",
OpLNot: "NOT",
OpJumpFalsy: "JMPF",
OpAndJump: "ANDJMP",
OpOrJump: "ORJMP",
OpJump: "JMP",
OpNull: "NULL",
OpGetGlobal: "GETG",
OpSetGlobal: "SETG",
OpSetSelGlobal: "SETSG",
OpArray: "ARR",
OpMap: "MAP",
OpError: "ERROR",
OpImmutable: "IMMUT",
OpIndex: "INDEX",
OpSliceIndex: "SLICE",
OpCall: "CALL",
OpReturn: "RET",
OpGetLocal: "GETL",
OpSetLocal: "SETL",
OpDefineLocal: "DEFL",
OpSetSelLocal: "SETSL",
OpGetBuiltin: "BUILTIN",
OpClosure: "CLOSURE",
OpGetFreePtr: "GETFP",
OpGetFree: "GETF",
OpSetFree: "SETF",
OpGetLocalPtr: "GETLP",
OpSetSelFree: "SETSF",
OpIteratorInit: "ITER",
OpIteratorNext: "ITNXT",
OpIteratorKey: "ITKEY",
OpIteratorValue: "ITVAL",
OpBinaryOp: "BINARYOP",
}
// OpcodeOperands is the number of operands.
var OpcodeOperands = [...][]int{
OpConstant: {2},
OpPop: {},
OpTrue: {},
OpFalse: {},
OpAdd: {},
OpSub: {},
OpMul: {},
OpDiv: {},
OpRem: {},
OpBAnd: {},
OpBOr: {},
OpBXor: {},
OpBAndNot: {},
OpBShiftLeft: {},
OpBShiftRight: {},
OpBComplement: {},
OpEqual: {},
OpNotEqual: {},
OpGreaterThan: {},
OpGreaterThanEqual: {},
OpMinus: {},
OpLNot: {},
OpJumpFalsy: {2},
OpAndJump: {2},
OpOrJump: {2},
OpJump: {2},
OpNull: {},
OpGetGlobal: {2},
OpSetGlobal: {2},
OpSetSelGlobal: {2, 1},
OpArray: {2},
OpMap: {2},
OpError: {},
OpImmutable: {},
OpIndex: {},
OpSliceIndex: {},
OpCall: {1},
OpReturn: {},
OpReturnValue: {},
OpGetLocal: {1},
OpSetLocal: {1},
OpDefineLocal: {1},
OpSetSelLocal: {1, 1},
OpGetBuiltin: {1},
OpGetBuiltinModule: {},
OpClosure: {2, 1},
OpGetFree: {1},
OpSetFree: {1},
OpSetSelFree: {1, 1},
OpIteratorInit: {},
OpIteratorNext: {},
OpIteratorKey: {},
OpIteratorValue: {},
OpConstant: {2},
OpPop: {},
OpTrue: {},
OpFalse: {},
OpBComplement: {},
OpEqual: {},
OpNotEqual: {},
OpMinus: {},
OpLNot: {},
OpJumpFalsy: {2},
OpAndJump: {2},
OpOrJump: {2},
OpJump: {2},
OpNull: {},
OpGetGlobal: {2},
OpSetGlobal: {2},
OpSetSelGlobal: {2, 1},
OpArray: {2},
OpMap: {2},
OpError: {},
OpImmutable: {},
OpIndex: {},
OpSliceIndex: {},
OpCall: {1},
OpReturn: {1},
OpGetLocal: {1},
OpSetLocal: {1},
OpDefineLocal: {1},
OpSetSelLocal: {1, 1},
OpGetBuiltin: {1},
OpClosure: {2, 1},
OpGetFreePtr: {1},
OpGetFree: {1},
OpSetFree: {1},
OpGetLocalPtr: {1},
OpSetSelFree: {1, 1},
OpIteratorInit: {},
OpIteratorNext: {},
OpIteratorKey: {},
OpIteratorValue: {},
OpBinaryOp: {1},
}
// ReadOperands reads operands from the bytecode.

View File

@ -1,28 +0,0 @@
package parser
import (
"io"
"github.com/d5/tengo/compiler/ast"
"github.com/d5/tengo/compiler/source"
)
// ParseFile parses a file with a given src.
func ParseFile(file *source.File, src []byte, trace io.Writer) (res *ast.File, err error) {
p := NewParser(file, src, trace)
defer func() {
if e := recover(); e != nil {
if _, ok := e.(bailout); !ok {
panic(e)
}
}
p.errors.Sort()
err = p.errors.Err()
}()
res, err = p.ParseFile()
return
}

View File

@ -12,5 +12,6 @@ func ParseSource(filename string, src []byte, trace io.Writer) (res *ast.File, e
fileSet := source.NewFileSet()
file := fileSet.AddFile(filename, -1, len(src))
return ParseFile(file, src, trace)
p := NewParser(file, src, trace)
return p.ParseFile()
}

View File

@ -57,7 +57,18 @@ func NewParser(file *source.File, src []byte, trace io.Writer) *Parser {
}
// ParseFile parses the source and returns an AST file unit.
func (p *Parser) ParseFile() (*ast.File, error) {
func (p *Parser) ParseFile() (file *ast.File, err error) {
defer func() {
if e := recover(); e != nil {
if _, ok := e.(bailout); !ok {
panic(e)
}
}
p.errors.Sort()
err = p.errors.Err()
}()
if p.trace {
defer un(trace(p, "File"))
}
@ -71,10 +82,12 @@ func (p *Parser) ParseFile() (*ast.File, error) {
return nil, p.errors.Err()
}
return &ast.File{
file = &ast.File{
InputFile: p.file,
Stmts: stmts,
}, nil
}
return
}
func (p *Parser) parseExpr() ast.Expr {
@ -1002,16 +1015,26 @@ func (p *Parser) parseMapElementLit() *ast.MapElementLit {
defer un(trace(p, "MapElementLit"))
}
// key: read identifier token but it's not actually an identifier
ident := p.parseIdent()
pos := p.pos
name := "_"
if p.token == token.Ident {
name = p.tokenLit
} else if p.token == token.String {
v, _ := strconv.Unquote(p.tokenLit)
name = v
} else {
p.errorExpected(pos, "map key")
}
p.next()
colonPos := p.expect(token.Colon)
valueExpr := p.parseExpr()
return &ast.MapElementLit{
Key: ident.Name,
KeyPos: ident.NamePos,
Key: name,
KeyPos: pos,
ColonPos: colonPos,
Value: valueExpr,
}

View File

@ -2,12 +2,13 @@ package compiler
// SymbolTable represents a symbol table.
type SymbolTable struct {
parent *SymbolTable
block bool
store map[string]*Symbol
numDefinition int
maxDefinition int
freeSymbols []*Symbol
parent *SymbolTable
block bool
store map[string]*Symbol
numDefinition int
maxDefinition int
freeSymbols []*Symbol
builtinSymbols []*Symbol
}
// NewSymbolTable creates a SymbolTable.
@ -37,6 +38,10 @@ func (t *SymbolTable) Define(name string) *Symbol {
// DefineBuiltin adds a symbol for builtin function.
func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
if t.parent != nil {
return t.parent.DefineBuiltin(index, name)
}
symbol := &Symbol{
Name: name,
Index: index,
@ -45,6 +50,8 @@ func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol {
t.store[name] = symbol
t.builtinSymbols = append(t.builtinSymbols, symbol)
return symbol
}
@ -57,9 +64,7 @@ func (t *SymbolTable) Resolve(name string) (symbol *Symbol, depth int, ok bool)
return
}
if !t.block {
depth++
}
depth++
// if symbol is defined in parent table and if it's not global/builtin
// then it's free variable.
@ -101,6 +106,15 @@ func (t *SymbolTable) FreeSymbols() []*Symbol {
return t.freeSymbols
}
// BuiltinSymbols returns builtin symbols for the scope.
func (t *SymbolTable) BuiltinSymbols() []*Symbol {
if t.parent != nil {
return t.parent.BuiltinSymbols()
}
return t.builtinSymbols
}
// Names returns the name of all the symbols.
func (t *SymbolTable) Names() []string {
var names []string

View File

@ -1,37 +0,0 @@
package objects
import "github.com/d5/tengo/compiler/token"
// Break represents a break statement.
type Break struct{}
// TypeName returns the name of the type.
func (o *Break) TypeName() string {
return "break"
}
func (o *Break) String() string {
return "<break>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *Break) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *Break) Copy() Object {
return &Break{}
}
// IsFalsy returns true if the value of the type is falsy.
func (o *Break) IsFalsy() bool {
return false
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *Break) Equals(x Object) bool {
return false
}

View File

@ -1,5 +1,7 @@
package objects
import "github.com/d5/tengo"
func builtinString(args ...Object) (Object, error) {
argsLen := len(args)
if !(argsLen == 1 || argsLen == 2) {
@ -12,6 +14,10 @@ func builtinString(args ...Object) (Object, error) {
v, ok := ToString(args[0])
if ok {
if len(v) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: v}, nil
}
@ -117,11 +123,19 @@ func builtinBytes(args ...Object) (Object, error) {
// bytes(N) => create a new bytes with given size N
if n, ok := args[0].(*Int); ok {
if n.Value > int64(tengo.MaxBytesLen) {
return nil, ErrBytesLimit
}
return &Bytes{Value: make([]byte, int(n.Value))}, nil
}
v, ok := ToByteSlice(args[0])
if ok {
if len(v) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: v}, nil
}

View File

@ -1,54 +0,0 @@
package objects
import (
"encoding/json"
)
// to_json(v object) => bytes
func builtinToJSON(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
res, err := json.Marshal(objectToInterface(args[0]))
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
return &Bytes{Value: res}, nil
}
// from_json(data string/bytes) => object
func builtinFromJSON(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
var target interface{}
switch o := args[0].(type) {
case *Bytes:
err := json.Unmarshal(o.Value, &target)
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
case *String:
err := json.Unmarshal([]byte(o.Value), &target)
if err != nil {
return &Error{Value: &String{Value: err.Error()}}, nil
}
default:
return nil, ErrInvalidArgumentType{
Name: "first",
Expected: "bytes/string",
Found: args[0].TypeName(),
}
}
res, err := FromInterface(target)
if err != nil {
return nil, err
}
return res, nil
}

23
vendor/github.com/d5/tengo/objects/builtin_module.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
package objects
// BuiltinModule is an importable module that's written in Go.
type BuiltinModule struct {
Attrs map[string]Object
}
// Import returns an immutable map for the module.
func (m *BuiltinModule) Import(moduleName string) (interface{}, error) {
return m.AsImmutableMap(moduleName), nil
}
// AsImmutableMap converts builtin module into an immutable map.
func (m *BuiltinModule) AsImmutableMap(moduleName string) *ImmutableMap {
attrs := make(map[string]Object, len(m.Attrs))
for k, v := range m.Attrs {
attrs[k] = v.Copy()
}
attrs["__module_name__"] = &String{Value: moduleName}
return &ImmutableMap{Value: attrs}
}

View File

@ -1,75 +0,0 @@
package objects
import (
"fmt"
)
// print(args...)
func builtinPrint(args ...Object) (Object, error) {
for _, arg := range args {
if str, ok := arg.(*String); ok {
fmt.Println(str.Value)
} else {
fmt.Println(arg.String())
}
}
return nil, nil
}
// printf("format", args...)
func builtinPrintf(args ...Object) (Object, error) {
numArgs := len(args)
if numArgs == 0 {
return nil, ErrWrongNumArguments
}
format, ok := args[0].(*String)
if !ok {
return nil, ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
fmt.Print(format)
return nil, nil
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objectToInterface(arg)
}
fmt.Printf(format.Value, formatArgs...)
return nil, nil
}
// sprintf("format", args...)
func builtinSprintf(args ...Object) (Object, error) {
numArgs := len(args)
if numArgs == 0 {
return nil, ErrWrongNumArguments
}
format, ok := args[0].(*String)
if !ok {
return nil, ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
return format, nil // okay to return 'format' directly as String is immutable
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objectToInterface(arg)
}
return &String{Value: fmt.Sprintf(format.Value, formatArgs...)}, nil
}

View File

@ -181,3 +181,15 @@ func builtinIsCallable(args ...Object) (Object, error) {
return FalseValue, nil
}
func builtinIsIterable(args ...Object) (Object, error) {
if len(args) != 1 {
return nil, ErrWrongNumArguments
}
if _, ok := args[0].(Iterable); ok {
return TrueValue, nil
}
return FalseValue, nil
}

View File

@ -1,135 +1,114 @@
package objects
// NamedBuiltinFunc is a named builtin function.
type NamedBuiltinFunc struct {
Name string
Func CallableFunc
}
// Builtins contains all default builtin functions.
var Builtins = []NamedBuiltinFunc{
// Use GetBuiltinFunctions instead of accessing Builtins directly.
var Builtins = []*BuiltinFunction{
{
Name: "print",
Func: builtinPrint,
Name: "len",
Value: builtinLen,
},
{
Name: "printf",
Func: builtinPrintf,
Name: "copy",
Value: builtinCopy,
},
{
Name: "sprintf",
Func: builtinSprintf,
Name: "append",
Value: builtinAppend,
},
{
Name: "len",
Func: builtinLen,
Name: "string",
Value: builtinString,
},
{
Name: "copy",
Func: builtinCopy,
Name: "int",
Value: builtinInt,
},
{
Name: "append",
Func: builtinAppend,
Name: "bool",
Value: builtinBool,
},
{
Name: "string",
Func: builtinString,
Name: "float",
Value: builtinFloat,
},
{
Name: "int",
Func: builtinInt,
Name: "char",
Value: builtinChar,
},
{
Name: "bool",
Func: builtinBool,
Name: "bytes",
Value: builtinBytes,
},
{
Name: "float",
Func: builtinFloat,
Name: "time",
Value: builtinTime,
},
{
Name: "char",
Func: builtinChar,
Name: "is_int",
Value: builtinIsInt,
},
{
Name: "bytes",
Func: builtinBytes,
Name: "is_float",
Value: builtinIsFloat,
},
{
Name: "time",
Func: builtinTime,
Name: "is_string",
Value: builtinIsString,
},
{
Name: "is_int",
Func: builtinIsInt,
Name: "is_bool",
Value: builtinIsBool,
},
{
Name: "is_float",
Func: builtinIsFloat,
Name: "is_char",
Value: builtinIsChar,
},
{
Name: "is_string",
Func: builtinIsString,
Name: "is_bytes",
Value: builtinIsBytes,
},
{
Name: "is_bool",
Func: builtinIsBool,
Name: "is_array",
Value: builtinIsArray,
},
{
Name: "is_char",
Func: builtinIsChar,
Name: "is_immutable_array",
Value: builtinIsImmutableArray,
},
{
Name: "is_bytes",
Func: builtinIsBytes,
Name: "is_map",
Value: builtinIsMap,
},
{
Name: "is_array",
Func: builtinIsArray,
Name: "is_immutable_map",
Value: builtinIsImmutableMap,
},
{
Name: "is_immutable_array",
Func: builtinIsImmutableArray,
Name: "is_iterable",
Value: builtinIsIterable,
},
{
Name: "is_map",
Func: builtinIsMap,
Name: "is_time",
Value: builtinIsTime,
},
{
Name: "is_immutable_map",
Func: builtinIsImmutableMap,
Name: "is_error",
Value: builtinIsError,
},
{
Name: "is_time",
Func: builtinIsTime,
Name: "is_undefined",
Value: builtinIsUndefined,
},
{
Name: "is_error",
Func: builtinIsError,
Name: "is_function",
Value: builtinIsFunction,
},
{
Name: "is_undefined",
Func: builtinIsUndefined,
Name: "is_callable",
Value: builtinIsCallable,
},
{
Name: "is_function",
Func: builtinIsFunction,
},
{
Name: "is_callable",
Func: builtinIsCallable,
},
{
Name: "to_json",
Func: builtinToJSON,
},
{
Name: "from_json",
Func: builtinFromJSON,
},
{
Name: "type_name",
Func: builtinTypeName,
Name: "type_name",
Value: builtinTypeName,
},
}

View File

@ -3,6 +3,7 @@ package objects
import (
"bytes"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/token"
)
@ -27,6 +28,10 @@ func (o *Bytes) BinaryOp(op token.Token, rhs Object) (Object, error) {
case token.Add:
switch rhs := rhs.(type) {
case *Bytes:
if len(o.Value)+len(rhs.Value) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: append(o.Value, rhs.Value...)}, nil
}
}
@ -74,3 +79,11 @@ func (o *Bytes) IndexGet(index Object) (res Object, err error) {
return
}
// Iterate creates a bytes iterator.
func (o *Bytes) Iterate() Iterator {
return &BytesIterator{
v: o.Value,
l: len(o.Value),
}
}

57
vendor/github.com/d5/tengo/objects/bytes_iterator.go generated vendored Normal file
View File

@ -0,0 +1,57 @@
package objects
import "github.com/d5/tengo/compiler/token"
// BytesIterator represents an iterator for a string.
type BytesIterator struct {
v []byte
i int
l int
}
// TypeName returns the name of the type.
func (i *BytesIterator) TypeName() string {
return "bytes-iterator"
}
func (i *BytesIterator) String() string {
return "<bytes-iterator>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (i *BytesIterator) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// IsFalsy returns true if the value of the type is falsy.
func (i *BytesIterator) IsFalsy() bool {
return true
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (i *BytesIterator) Equals(Object) bool {
return false
}
// Copy returns a copy of the type.
func (i *BytesIterator) Copy() Object {
return &BytesIterator{v: i.v, i: i.i, l: i.l}
}
// Next returns true if there are more elements to iterate.
func (i *BytesIterator) Next() bool {
i.i++
return i.i <= i.l
}
// Key returns the key or index value of the current element.
func (i *BytesIterator) Key() Object {
return &Int{Value: int64(i.i - 1)}
}
// Value returns the value of the current element.
func (i *BytesIterator) Value() Object {
return &Int{Value: int64(i.v[i.i-1])}
}

View File

@ -1,4 +1,4 @@
package objects
// CallableFunc is a function signature for the callable functions.
type CallableFunc func(args ...Object) (ret Object, err error)
type CallableFunc = func(args ...Object) (ret Object, err error)

View File

@ -7,7 +7,7 @@ import (
// Closure represents a function closure.
type Closure struct {
Fn *CompiledFunction
Free []*Object
Free []*ObjectPtr
}
// TypeName returns the name of the type.
@ -29,7 +29,7 @@ func (o *Closure) BinaryOp(op token.Token, rhs Object) (Object, error) {
func (o *Closure) Copy() Object {
return &Closure{
Fn: o.Fn.Copy().(*CompiledFunction),
Free: append([]*Object{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers
Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers
}
}

View File

@ -47,3 +47,14 @@ func (o *CompiledFunction) IsFalsy() bool {
func (o *CompiledFunction) Equals(x Object) bool {
return false
}
// SourcePos returns the source position of the instruction at ip.
func (o *CompiledFunction) SourcePos(ip int) source.Pos {
for ip >= 0 {
if p, ok := o.SourceMap[ip]; ok {
return p
}
ip--
}
return source.NoPos
}

View File

@ -1,38 +0,0 @@
package objects
import "github.com/d5/tengo/compiler/token"
// Continue represents a continue statement.
type Continue struct {
}
// TypeName returns the name of the type.
func (o *Continue) TypeName() string {
return "continue"
}
func (o *Continue) String() string {
return "<continue>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *Continue) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *Continue) Copy() Object {
return &Continue{}
}
// IsFalsy returns true if the value of the type is falsy.
func (o *Continue) IsFalsy() bool {
return false
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *Continue) Equals(x Object) bool {
return false
}

View File

@ -1,9 +1,12 @@
package objects
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/d5/tengo"
)
// ToString will try to convert object o to string value.
@ -156,8 +159,8 @@ func ToTime(o Object) (v time.Time, ok bool) {
return
}
// objectToInterface attempts to convert an object o to an interface{} value
func objectToInterface(o Object) (res interface{}) {
// ToInterface attempts to convert an object o to an interface{} value
func ToInterface(o Object) (res interface{}) {
switch o := o.(type) {
case *Int:
res = o.Value
@ -174,13 +177,29 @@ func objectToInterface(o Object) (res interface{}) {
case *Array:
res = make([]interface{}, len(o.Value))
for i, val := range o.Value {
res.([]interface{})[i] = objectToInterface(val)
res.([]interface{})[i] = ToInterface(val)
}
case *ImmutableArray:
res = make([]interface{}, len(o.Value))
for i, val := range o.Value {
res.([]interface{})[i] = ToInterface(val)
}
case *Map:
res = make(map[string]interface{})
for key, v := range o.Value {
res.(map[string]interface{})[key] = objectToInterface(v)
res.(map[string]interface{})[key] = ToInterface(v)
}
case *ImmutableMap:
res = make(map[string]interface{})
for key, v := range o.Value {
res.(map[string]interface{})[key] = ToInterface(v)
}
case *Time:
res = o.Value
case *Error:
res = errors.New(o.String())
case *Undefined:
res = nil
case Object:
return o
}
@ -194,6 +213,9 @@ func FromInterface(v interface{}) (Object, error) {
case nil:
return UndefinedValue, nil
case string:
if len(v) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: v}, nil
case int64:
return &Int{Value: v}, nil
@ -211,6 +233,9 @@ func FromInterface(v interface{}) (Object, error) {
case float64:
return &Float{Value: v}, nil
case []byte:
if len(v) > tengo.MaxBytesLen {
return nil, ErrBytesLimit
}
return &Bytes{Value: v}, nil
case error:
return &Error{Value: &String{Value: v.Error()}}, nil
@ -243,6 +268,8 @@ func FromInterface(v interface{}) (Object, error) {
return &Time{Value: v}, nil
case Object:
return v, nil
case CallableFunc:
return &UserFunction{Value: v}, nil
}
return nil, fmt.Errorf("cannot convert to object: %T", v)

31
vendor/github.com/d5/tengo/objects/count_objects.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
package objects
// CountObjects returns the number of objects that a given object o contains.
// For scalar value types, it will always be 1. For compound value types,
// this will include its elements and all of their elements recursively.
func CountObjects(o Object) (c int) {
c = 1
switch o := o.(type) {
case *Array:
for _, v := range o.Value {
c += CountObjects(v)
}
case *ImmutableArray:
for _, v := range o.Value {
c += CountObjects(v)
}
case *Map:
for _, v := range o.Value {
c += CountObjects(v)
}
case *ImmutableMap:
for _, v := range o.Value {
c += CountObjects(v)
}
case *Error:
c += CountObjects(o.Value)
}
return
}

View File

@ -20,6 +20,12 @@ var ErrInvalidOperator = errors.New("invalid operator")
// ErrWrongNumArguments represents a wrong number of arguments error.
var ErrWrongNumArguments = errors.New("wrong number of arguments")
// ErrBytesLimit represents an error where the size of bytes value exceeds the limit.
var ErrBytesLimit = errors.New("exceeding bytes size limit")
// ErrStringLimit represents an error where the size of string value exceeds the limit.
var ErrStringLimit = errors.New("exceeding string size limit")
// ErrInvalidArgumentType represents an invalid argument value type error.
type ErrInvalidArgumentType struct {
Name string

7
vendor/github.com/d5/tengo/objects/importable.go generated vendored Normal file
View File

@ -0,0 +1,7 @@
package objects
// Importable interface represents importable module instance.
type Importable interface {
// Import should return either an Object or module source code ([]byte).
Import(moduleName string) (interface{}, error)
}

77
vendor/github.com/d5/tengo/objects/module_map.go generated vendored Normal file
View File

@ -0,0 +1,77 @@
package objects
// ModuleMap represents a set of named modules.
// Use NewModuleMap to create a new module map.
type ModuleMap struct {
m map[string]Importable
}
// NewModuleMap creates a new module map.
func NewModuleMap() *ModuleMap {
return &ModuleMap{
m: make(map[string]Importable),
}
}
// Add adds an import module.
func (m *ModuleMap) Add(name string, module Importable) {
m.m[name] = module
}
// AddBuiltinModule adds a builtin module.
func (m *ModuleMap) AddBuiltinModule(name string, attrs map[string]Object) {
m.m[name] = &BuiltinModule{Attrs: attrs}
}
// AddSourceModule adds a source module.
func (m *ModuleMap) AddSourceModule(name string, src []byte) {
m.m[name] = &SourceModule{Src: src}
}
// Remove removes a named module.
func (m *ModuleMap) Remove(name string) {
delete(m.m, name)
}
// Get returns an import module identified by name.
// It returns if the name is not found.
func (m *ModuleMap) Get(name string) Importable {
return m.m[name]
}
// GetBuiltinModule returns a builtin module identified by name.
// It returns if the name is not found or the module is not a builtin module.
func (m *ModuleMap) GetBuiltinModule(name string) *BuiltinModule {
mod, _ := m.m[name].(*BuiltinModule)
return mod
}
// GetSourceModule returns a source module identified by name.
// It returns if the name is not found or the module is not a source module.
func (m *ModuleMap) GetSourceModule(name string) *SourceModule {
mod, _ := m.m[name].(*SourceModule)
return mod
}
// Copy creates a copy of the module map.
func (m *ModuleMap) Copy() *ModuleMap {
c := &ModuleMap{
m: make(map[string]Importable),
}
for name, mod := range m.m {
c.m[name] = mod
}
return c
}
// Len returns the number of named modules.
func (m *ModuleMap) Len() int {
return len(m.m)
}
// AddMap adds named modules from another module map.
func (m *ModuleMap) AddMap(o *ModuleMap) {
for name, mod := range o.m {
m.m[name] = mod
}
}

41
vendor/github.com/d5/tengo/objects/object_ptr.go generated vendored Normal file
View File

@ -0,0 +1,41 @@
package objects
import (
"github.com/d5/tengo/compiler/token"
)
// ObjectPtr represents a free variable.
type ObjectPtr struct {
Value *Object
}
func (o *ObjectPtr) String() string {
return "free-var"
}
// TypeName returns the name of the type.
func (o *ObjectPtr) TypeName() string {
return "<free-var>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *ObjectPtr) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *ObjectPtr) Copy() Object {
return o
}
// IsFalsy returns true if the value of the type is falsy.
func (o *ObjectPtr) IsFalsy() bool {
return o.Value == nil
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *ObjectPtr) Equals(x Object) bool {
return o == x
}

View File

@ -1,39 +0,0 @@
package objects
import "github.com/d5/tengo/compiler/token"
// ReturnValue represents a value that is being returned.
type ReturnValue struct {
Value Object
}
// TypeName returns the name of the type.
func (o *ReturnValue) TypeName() string {
return "return-value"
}
func (o *ReturnValue) String() string {
return "<return-value>"
}
// BinaryOp returns another object that is the result of
// a given binary operator and a right-hand side object.
func (o *ReturnValue) BinaryOp(op token.Token, rhs Object) (Object, error) {
return nil, ErrInvalidOperator
}
// Copy returns a copy of the type.
func (o *ReturnValue) Copy() Object {
return &ReturnValue{Value: o.Copy()}
}
// IsFalsy returns true if the value of the type is falsy.
func (o *ReturnValue) IsFalsy() bool {
return false
}
// Equals returns true if the value of the type
// is equal to the value of another object.
func (o *ReturnValue) Equals(x Object) bool {
return false
}

11
vendor/github.com/d5/tengo/objects/source_module.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
package objects
// SourceModule is an importable module that's written in Tengo.
type SourceModule struct {
Src []byte
}
// Import returns a module source code.
func (m *SourceModule) Import(_ string) (interface{}, error) {
return m.Src, nil
}

View File

@ -3,6 +3,7 @@ package objects
import (
"strconv"
"github.com/d5/tengo"
"github.com/d5/tengo/compiler/token"
)
@ -28,9 +29,16 @@ func (o *String) BinaryOp(op token.Token, rhs Object) (Object, error) {
case token.Add:
switch rhs := rhs.(type) {
case *String:
if len(o.Value)+len(rhs.Value) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: o.Value + rhs.Value}, nil
default:
return &String{Value: o.Value + rhs.String()}, nil
rhsStr := rhs.String()
if len(o.Value)+len(rhsStr) > tengo.MaxStringLen {
return nil, ErrStringLimit
}
return &String{Value: o.Value + rhsStr}, nil
}
}

View File

@ -6,8 +6,9 @@ import (
// UserFunction represents a user function.
type UserFunction struct {
Name string
Value CallableFunc
Name string
Value CallableFunc
EncodingID string
}
// TypeName returns the name of the type.

View File

@ -6,3 +6,6 @@ import (
// ErrStackOverflow is a stack overflow error.
var ErrStackOverflow = errors.New("stack overflow")
// ErrObjectAllocLimit is an objects allocation limit error.
var ErrObjectAllocLimit = errors.New("object allocation limit exceeded")

View File

@ -7,7 +7,7 @@ import (
// Frame represents a function call frame.
type Frame struct {
fn *objects.CompiledFunction
freeVars []*objects.Object
freeVars []*objects.ObjectPtr
ip int
basePointer int
}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ package script
import (
"context"
"fmt"
"sync"
"github.com/d5/tengo/compiler"
"github.com/d5/tengo/objects"
@ -12,26 +13,39 @@ import (
// Compiled is a compiled instance of the user script.
// Use Script.Compile() to create Compiled object.
type Compiled struct {
symbolTable *compiler.SymbolTable
machine *runtime.VM
globalIndexes map[string]int // global symbol name to index
bytecode *compiler.Bytecode
globals []objects.Object
maxAllocs int64
lock sync.RWMutex
}
// Run executes the compiled script in the virtual machine.
func (c *Compiled) Run() error {
return c.machine.Run()
c.lock.Lock()
defer c.lock.Unlock()
v := runtime.NewVM(c.bytecode, c.globals, c.maxAllocs)
return v.Run()
}
// RunContext is like Run but includes a context.
func (c *Compiled) RunContext(ctx context.Context) (err error) {
c.lock.Lock()
defer c.lock.Unlock()
v := runtime.NewVM(c.bytecode, c.globals, c.maxAllocs)
ch := make(chan error, 1)
go func() {
ch <- c.machine.Run()
ch <- v.Run()
}()
select {
case <-ctx.Done():
c.machine.Abort()
v.Abort()
<-ch
err = ctx.Err()
case err = <-ch:
@ -40,30 +54,58 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
return
}
// Clone creates a new copy of Compiled.
// Cloned copies are safe for concurrent use by multiple goroutines.
func (c *Compiled) Clone() *Compiled {
c.lock.Lock()
defer c.lock.Unlock()
clone := &Compiled{
globalIndexes: c.globalIndexes,
bytecode: c.bytecode,
globals: make([]objects.Object, len(c.globals)),
maxAllocs: c.maxAllocs,
}
// copy global objects
for idx, g := range c.globals {
if g != nil {
clone.globals[idx] = g
}
}
return clone
}
// IsDefined returns true if the variable name is defined (has value) before or after the execution.
func (c *Compiled) IsDefined(name string) bool {
symbol, _, ok := c.symbolTable.Resolve(name)
c.lock.RLock()
defer c.lock.RUnlock()
idx, ok := c.globalIndexes[name]
if !ok {
return false
}
v := c.machine.Globals()[symbol.Index]
v := c.globals[idx]
if v == nil {
return false
}
return *v != objects.UndefinedValue
return v != objects.UndefinedValue
}
// Get returns a variable identified by the name.
func (c *Compiled) Get(name string) *Variable {
value := &objects.UndefinedValue
c.lock.RLock()
defer c.lock.RUnlock()
symbol, _, ok := c.symbolTable.Resolve(name)
if ok && symbol.Scope == compiler.ScopeGlobal {
value = c.machine.Globals()[symbol.Index]
value := objects.UndefinedValue
if idx, ok := c.globalIndexes[name]; ok {
value = c.globals[idx]
if value == nil {
value = &objects.UndefinedValue
value = objects.UndefinedValue
}
}
@ -75,20 +117,21 @@ func (c *Compiled) Get(name string) *Variable {
// GetAll returns all the variables that are defined by the compiled script.
func (c *Compiled) GetAll() []*Variable {
var vars []*Variable
for _, name := range c.symbolTable.Names() {
symbol, _, ok := c.symbolTable.Resolve(name)
if ok && symbol.Scope == compiler.ScopeGlobal {
value := c.machine.Globals()[symbol.Index]
if value == nil {
value = &objects.UndefinedValue
}
c.lock.RLock()
defer c.lock.RUnlock()
vars = append(vars, &Variable{
name: name,
value: value,
})
var vars []*Variable
for name, idx := range c.globalIndexes {
value := c.globals[idx]
if value == nil {
value = objects.UndefinedValue
}
vars = append(vars, &Variable{
name: name,
value: value,
})
}
return vars
@ -97,17 +140,20 @@ func (c *Compiled) GetAll() []*Variable {
// Set replaces the value of a global variable identified by the name.
// An error will be returned if the name was not defined during compilation.
func (c *Compiled) Set(name string, value interface{}) error {
c.lock.Lock()
defer c.lock.Unlock()
obj, err := objects.FromInterface(value)
if err != nil {
return err
}
symbol, _, ok := c.symbolTable.Resolve(name)
if !ok || symbol.Scope != compiler.ScopeGlobal {
idx, ok := c.globalIndexes[name]
if !ok {
return fmt.Errorf("'%s' is not defined", name)
}
c.machine.Globals()[symbol.Index] = &obj
c.globals[idx] = obj
return nil
}

View File

@ -1,33 +0,0 @@
package script
import (
"github.com/d5/tengo/objects"
)
func objectToInterface(o objects.Object) interface{} {
switch val := o.(type) {
case *objects.Array:
return val.Value
case *objects.Map:
return val.Value
case *objects.Int:
return val.Value
case *objects.Float:
return val.Value
case *objects.Bool:
if val == objects.TrueValue {
return true
}
return false
case *objects.Char:
return val.Value
case *objects.String:
return val.Value
case *objects.Bytes:
return val.Value
case *objects.Undefined:
return nil
}
return o
}

View File

@ -9,23 +9,25 @@ import (
"github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/objects"
"github.com/d5/tengo/runtime"
"github.com/d5/tengo/stdlib"
)
// Script can simplify compilation and execution of embedded scripts.
type Script struct {
variables map[string]*Variable
removedBuiltins map[string]bool
removedStdModules map[string]bool
userModuleLoader compiler.ModuleLoader
input []byte
variables map[string]*Variable
modules *objects.ModuleMap
input []byte
maxAllocs int64
maxConstObjects int
enableFileImport bool
}
// New creates a Script instance with an input script.
func New(input []byte) *Script {
return &Script{
variables: make(map[string]*Variable),
input: input,
variables: make(map[string]*Variable),
input: input,
maxAllocs: -1,
maxConstObjects: -1,
}
}
@ -38,7 +40,7 @@ func (s *Script) Add(name string, value interface{}) error {
s.variables[name] = &Variable{
name: name,
value: &obj,
value: obj,
}
return nil
@ -56,32 +58,31 @@ func (s *Script) Remove(name string) bool {
return true
}
// DisableBuiltinFunction disables a builtin function.
func (s *Script) DisableBuiltinFunction(name string) {
if s.removedBuiltins == nil {
s.removedBuiltins = make(map[string]bool)
}
s.removedBuiltins[name] = true
// SetImports sets import modules.
func (s *Script) SetImports(modules *objects.ModuleMap) {
s.modules = modules
}
// DisableStdModule disables a standard library module.
func (s *Script) DisableStdModule(name string) {
if s.removedStdModules == nil {
s.removedStdModules = make(map[string]bool)
}
s.removedStdModules[name] = true
// SetMaxAllocs sets the maximum number of objects allocations during the run time.
// Compiled script will return runtime.ErrObjectAllocLimit error if it exceeds this limit.
func (s *Script) SetMaxAllocs(n int64) {
s.maxAllocs = n
}
// SetUserModuleLoader sets the user module loader for the compiler.
func (s *Script) SetUserModuleLoader(loader compiler.ModuleLoader) {
s.userModuleLoader = loader
// SetMaxConstObjects sets the maximum number of objects in the compiled constants.
func (s *Script) SetMaxConstObjects(n int) {
s.maxConstObjects = n
}
// EnableFileImport enables or disables module loading from local files.
// Local file modules are disabled by default.
func (s *Script) EnableFileImport(enable bool) {
s.enableFileImport = enable
}
// Compile compiles the script with all the defined variables, and, returns Compiled object.
func (s *Script) Compile() (*Compiled, error) {
symbolTable, stdModules, globals, err := s.prepCompile()
symbolTable, globals, err := s.prepCompile()
if err != nil {
return nil, err
}
@ -92,22 +93,44 @@ func (s *Script) Compile() (*Compiled, error) {
p := parser.NewParser(srcFile, s.input, nil)
file, err := p.ParseFile()
if err != nil {
return nil, fmt.Errorf("parse error: %s", err.Error())
}
c := compiler.NewCompiler(srcFile, symbolTable, nil, stdModules, nil)
if s.userModuleLoader != nil {
c.SetModuleLoader(s.userModuleLoader)
return nil, err
}
c := compiler.NewCompiler(srcFile, symbolTable, nil, s.modules, nil)
c.EnableFileImport(s.enableFileImport)
if err := c.Compile(file); err != nil {
return nil, err
}
// reduce globals size
globals = globals[:symbolTable.MaxSymbols()+1]
// global symbol names to indexes
globalIndexes := make(map[string]int, len(globals))
for _, name := range symbolTable.Names() {
symbol, _, _ := symbolTable.Resolve(name)
if symbol.Scope == compiler.ScopeGlobal {
globalIndexes[name] = symbol.Index
}
}
// remove duplicates from constants
bytecode := c.Bytecode()
bytecode.RemoveDuplicates()
// check the constant objects limit
if s.maxConstObjects >= 0 {
cnt := bytecode.CountObjects()
if cnt > s.maxConstObjects {
return nil, fmt.Errorf("exceeding constant objects limit: %d", cnt)
}
}
return &Compiled{
symbolTable: symbolTable,
machine: runtime.NewVM(c.Bytecode(), globals, nil),
globalIndexes: globalIndexes,
bytecode: bytecode,
globals: globals,
maxAllocs: s.maxAllocs,
}, nil
}
@ -136,7 +159,7 @@ func (s *Script) RunContext(ctx context.Context) (compiled *Compiled, err error)
return
}
func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, stdModules map[string]bool, globals []*objects.Object, err error) {
func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, globals []objects.Object, err error) {
var names []string
for name := range s.variables {
names = append(names, name)
@ -144,19 +167,10 @@ func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, stdModules ma
symbolTable = compiler.NewSymbolTable()
for idx, fn := range objects.Builtins {
if !s.removedBuiltins[fn.Name] {
symbolTable.DefineBuiltin(idx, fn.Name)
}
symbolTable.DefineBuiltin(idx, fn.Name)
}
stdModules = make(map[string]bool)
for name := range stdlib.Modules {
if !s.removedStdModules[name] {
stdModules[name] = true
}
}
globals = make([]*objects.Object, runtime.GlobalsSize, runtime.GlobalsSize)
globals = make([]objects.Object, runtime.GlobalsSize)
for idx, name := range names {
symbol := symbolTable.Define(name)

View File

@ -9,7 +9,7 @@ import (
// Variable is a user-defined variable for the script.
type Variable struct {
name string
value *objects.Object
value objects.Object
}
// NewVariable creates a Variable.
@ -21,7 +21,7 @@ func NewVariable(name string, value interface{}) (*Variable, error) {
return &Variable{
name: name,
value: &obj,
value: obj,
}, nil
}
@ -32,18 +32,18 @@ func (v *Variable) Name() string {
// Value returns an empty interface of the variable value.
func (v *Variable) Value() interface{} {
return objectToInterface(*v.value)
return objects.ToInterface(v.value)
}
// ValueType returns the name of the value type.
func (v *Variable) ValueType() string {
return (*v.value).TypeName()
return v.value.TypeName()
}
// Int returns int value of the variable value.
// It returns 0 if the value is not convertible to int.
func (v *Variable) Int() int {
c, _ := objects.ToInt(*v.value)
c, _ := objects.ToInt(v.value)
return c
}
@ -51,7 +51,7 @@ func (v *Variable) Int() int {
// Int64 returns int64 value of the variable value.
// It returns 0 if the value is not convertible to int64.
func (v *Variable) Int64() int64 {
c, _ := objects.ToInt64(*v.value)
c, _ := objects.ToInt64(v.value)
return c
}
@ -59,7 +59,7 @@ func (v *Variable) Int64() int64 {
// Float returns float64 value of the variable value.
// It returns 0.0 if the value is not convertible to float64.
func (v *Variable) Float() float64 {
c, _ := objects.ToFloat64(*v.value)
c, _ := objects.ToFloat64(v.value)
return c
}
@ -67,7 +67,7 @@ func (v *Variable) Float() float64 {
// Char returns rune value of the variable value.
// It returns 0 if the value is not convertible to rune.
func (v *Variable) Char() rune {
c, _ := objects.ToRune(*v.value)
c, _ := objects.ToRune(v.value)
return c
}
@ -75,7 +75,7 @@ func (v *Variable) Char() rune {
// Bool returns bool value of the variable value.
// It returns 0 if the value is not convertible to bool.
func (v *Variable) Bool() bool {
c, _ := objects.ToBool(*v.value)
c, _ := objects.ToBool(v.value)
return c
}
@ -83,11 +83,11 @@ func (v *Variable) Bool() bool {
// Array returns []interface value of the variable value.
// It returns 0 if the value is not convertible to []interface.
func (v *Variable) Array() []interface{} {
switch val := (*v.value).(type) {
switch val := v.value.(type) {
case *objects.Array:
var arr []interface{}
for _, e := range val.Value {
arr = append(arr, objectToInterface(e))
arr = append(arr, objects.ToInterface(e))
}
return arr
}
@ -98,11 +98,11 @@ func (v *Variable) Array() []interface{} {
// Map returns map[string]interface{} value of the variable value.
// It returns 0 if the value is not convertible to map[string]interface{}.
func (v *Variable) Map() map[string]interface{} {
switch val := (*v.value).(type) {
switch val := v.value.(type) {
case *objects.Map:
kv := make(map[string]interface{})
for mk, mv := range val.Value {
kv[mk] = objectToInterface(mv)
kv[mk] = objects.ToInterface(mv)
}
return kv
}
@ -113,7 +113,7 @@ func (v *Variable) Map() map[string]interface{} {
// String returns string value of the variable value.
// It returns 0 if the value is not convertible to string.
func (v *Variable) String() string {
c, _ := objects.ToString(*v.value)
c, _ := objects.ToString(v.value)
return c
}
@ -121,7 +121,7 @@ func (v *Variable) String() string {
// Bytes returns a byte slice of the variable value.
// It returns nil if the value is not convertible to byte slice.
func (v *Variable) Bytes() []byte {
c, _ := objects.ToByteSlice(*v.value)
c, _ := objects.ToByteSlice(v.value)
return c
}
@ -129,7 +129,7 @@ func (v *Variable) Bytes() []byte {
// Error returns an error if the underlying value is error object.
// If not, this returns nil.
func (v *Variable) Error() error {
err, ok := (*v.value).(*objects.Error)
err, ok := v.value.(*objects.Error)
if ok {
return errors.New(err.String())
}
@ -140,10 +140,10 @@ func (v *Variable) Error() error {
// Object returns an underlying Object of the variable value.
// Note that returned Object is a copy of an actual Object used in the script.
func (v *Variable) Object() objects.Object {
return *v.value
return v.value
}
// IsUndefined returns true if the underlying value is undefined.
func (v *Variable) IsUndefined() bool {
return *v.value == objects.UndefinedValue
return v.value == objects.UndefinedValue
}

14
vendor/github.com/d5/tengo/stdlib/builtin_modules.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
package stdlib
import "github.com/d5/tengo/objects"
// BuiltinModules are builtin type standard library modules.
var BuiltinModules = map[string]map[string]objects.Object{
"math": mathModule,
"os": osModule,
"text": textModule,
"times": timesModule,
"rand": randModule,
"fmt": fmtModule,
"json": jsonModule,
}

116
vendor/github.com/d5/tengo/stdlib/fmt.go generated vendored Normal file
View File

@ -0,0 +1,116 @@
package stdlib
import (
"fmt"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
var fmtModule = map[string]objects.Object{
"print": &objects.UserFunction{Name: "print", Value: fmtPrint},
"printf": &objects.UserFunction{Name: "printf", Value: fmtPrintf},
"println": &objects.UserFunction{Name: "println", Value: fmtPrintln},
"sprintf": &objects.UserFunction{Name: "sprintf", Value: fmtSprintf},
}
func fmtPrint(args ...objects.Object) (ret objects.Object, err error) {
printArgs, err := getPrintArgs(args...)
if err != nil {
return nil, err
}
_, _ = fmt.Print(printArgs...)
return nil, nil
}
func fmtPrintf(args ...objects.Object) (ret objects.Object, err error) {
numArgs := len(args)
if numArgs == 0 {
return nil, objects.ErrWrongNumArguments
}
format, ok := args[0].(*objects.String)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
fmt.Print(format)
return nil, nil
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objects.ToInterface(arg)
}
fmt.Printf(format.Value, formatArgs...)
return nil, nil
}
func fmtPrintln(args ...objects.Object) (ret objects.Object, err error) {
printArgs, err := getPrintArgs(args...)
if err != nil {
return nil, err
}
printArgs = append(printArgs, "\n")
_, _ = fmt.Print(printArgs...)
return nil, nil
}
func fmtSprintf(args ...objects.Object) (ret objects.Object, err error) {
numArgs := len(args)
if numArgs == 0 {
return nil, objects.ErrWrongNumArguments
}
format, ok := args[0].(*objects.String)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "format",
Expected: "string",
Found: args[0].TypeName(),
}
}
if numArgs == 1 {
return format, nil // okay to return 'format' directly as String is immutable
}
formatArgs := make([]interface{}, numArgs-1, numArgs-1)
for idx, arg := range args[1:] {
formatArgs[idx] = objects.ToInterface(arg)
}
s := fmt.Sprintf(format.Value, formatArgs...)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
func getPrintArgs(args ...objects.Object) ([]interface{}, error) {
var printArgs []interface{}
l := 0
for _, arg := range args {
s, _ := objects.ToString(arg)
slen := len(s)
if l+slen > tengo.MaxStringLen { // make sure length does not exceed the limit
return nil, objects.ErrStringLimit
}
l += slen
printArgs = append(printArgs, s)
}
return printArgs, nil
}

View File

@ -3,6 +3,7 @@ package stdlib
import (
"fmt"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -124,7 +125,13 @@ func FuncARS(fn func() string) objects.CallableFunc {
return nil, objects.ErrWrongNumArguments
}
return &objects.String{Value: fn()}, nil
s := fn()
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -141,6 +148,10 @@ func FuncARSE(fn func() (string, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
}
@ -158,6 +169,10 @@ func FuncARYE(fn func() ([]byte, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxBytesLen {
return nil, objects.ErrBytesLimit
}
return &objects.Bytes{Value: res}, nil
}
}
@ -183,8 +198,12 @@ func FuncARSs(fn func() []string) objects.CallableFunc {
}
arr := &objects.Array{}
for _, osArg := range fn() {
arr.Value = append(arr.Value, &objects.String{Value: osArg})
for _, elem := range fn() {
if len(elem) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: elem})
}
return arr, nil
@ -493,7 +512,13 @@ func FuncASRS(fn func(string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1)}, nil
s := fn(s1)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -516,8 +541,12 @@ func FuncASRSs(fn func(string) []string) objects.CallableFunc {
res := fn(s1)
arr := &objects.Array{}
for _, osArg := range res {
arr.Value = append(arr.Value, &objects.String{Value: osArg})
for _, elem := range res {
if len(elem) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: elem})
}
return arr, nil
@ -546,6 +575,10 @@ func FuncASRSE(fn func(string) (string, error)) objects.CallableFunc {
return wrapError(err), nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
}
@ -628,6 +661,10 @@ func FuncASSRSs(fn func(string, string) []string) objects.CallableFunc {
arr := &objects.Array{}
for _, res := range fn(s1, s2) {
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: res})
}
@ -671,6 +708,10 @@ func FuncASSIRSs(fn func(string, string, int) []string) objects.CallableFunc {
arr := &objects.Array{}
for _, res := range fn(s1, s2, i3) {
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: res})
}
@ -732,7 +773,13 @@ func FuncASSRS(fn func(string, string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1, s2)}, nil
s := fn(s1, s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -819,7 +866,12 @@ func FuncASsSRS(fn func([]string, string) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(ss1, s2)}, nil
s := fn(ss1, s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -909,7 +961,13 @@ func FuncASIRS(fn func(string, int) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(s1, i2)}, nil
s := fn(s1, i2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}
@ -1028,6 +1086,10 @@ func FuncAIRSsE(fn func(int) ([]string, error)) objects.CallableFunc {
arr := &objects.Array{}
for _, r := range res {
if len(r) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: r})
}
@ -1052,6 +1114,12 @@ func FuncAIRS(fn func(int) string) objects.CallableFunc {
}
}
return &objects.String{Value: fn(i1)}, nil
s := fn(i1)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
}

53
vendor/github.com/d5/tengo/stdlib/gensrcmods.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
// +build ignore
package main
import (
"bytes"
"io/ioutil"
"log"
"regexp"
"strconv"
)
var tengoModFileRE = regexp.MustCompile(`^srcmod_(\w+).tengo$`)
func main() {
modules := make(map[string]string)
// enumerate all Tengo module files
files, err := ioutil.ReadDir(".")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
m := tengoModFileRE.FindStringSubmatch(file.Name())
if m != nil {
modName := m[1]
src, err := ioutil.ReadFile(file.Name())
if err != nil {
log.Fatalf("file '%s' read error: %s", file.Name(), err.Error())
}
modules[modName] = string(src)
}
}
var out bytes.Buffer
out.WriteString(`// Code generated using gensrcmods.go; DO NOT EDIT.
package stdlib
// SourceModules are source type standard library modules.
var SourceModules = map[string]string{` + "\n")
for modName, modSrc := range modules {
out.WriteString("\t\"" + modName + "\": " + strconv.Quote(modSrc) + ",\n")
}
out.WriteString("}\n")
const target = "source_modules.go"
if err := ioutil.WriteFile(target, out.Bytes(), 0644); err != nil {
log.Fatal(err)
}
}

126
vendor/github.com/d5/tengo/stdlib/json.go generated vendored Normal file
View File

@ -0,0 +1,126 @@
package stdlib
import (
"bytes"
gojson "encoding/json"
"github.com/d5/tengo/objects"
"github.com/d5/tengo/stdlib/json"
)
var jsonModule = map[string]objects.Object{
"decode": &objects.UserFunction{Name: "decode", Value: jsonDecode},
"encode": &objects.UserFunction{Name: "encode", Value: jsonEncode},
"indent": &objects.UserFunction{Name: "encode", Value: jsonIndent},
"html_escape": &objects.UserFunction{Name: "html_escape", Value: jsonHTMLEscape},
}
func jsonDecode(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
switch o := args[0].(type) {
case *objects.Bytes:
v, err := json.Decode(o.Value)
if err != nil {
return &objects.Error{Value: &objects.String{Value: err.Error()}}, nil
}
return v, nil
case *objects.String:
v, err := json.Decode([]byte(o.Value))
if err != nil {
return &objects.Error{Value: &objects.String{Value: err.Error()}}, nil
}
return v, nil
default:
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "bytes/string",
Found: args[0].TypeName(),
}
}
}
func jsonEncode(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
b, err := json.Encode(args[0])
if err != nil {
return &objects.Error{Value: &objects.String{Value: err.Error()}}, nil
}
return &objects.Bytes{Value: b}, nil
}
func jsonIndent(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 3 {
return nil, objects.ErrWrongNumArguments
}
prefix, ok := objects.ToString(args[1])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "prefix",
Expected: "string(compatible)",
Found: args[1].TypeName(),
}
}
indent, ok := objects.ToString(args[2])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "indent",
Expected: "string(compatible)",
Found: args[2].TypeName(),
}
}
switch o := args[0].(type) {
case *objects.Bytes:
var dst bytes.Buffer
err := gojson.Indent(&dst, o.Value, prefix, indent)
if err != nil {
return &objects.Error{Value: &objects.String{Value: err.Error()}}, nil
}
return &objects.Bytes{Value: dst.Bytes()}, nil
case *objects.String:
var dst bytes.Buffer
err := gojson.Indent(&dst, []byte(o.Value), prefix, indent)
if err != nil {
return &objects.Error{Value: &objects.String{Value: err.Error()}}, nil
}
return &objects.Bytes{Value: dst.Bytes()}, nil
default:
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "bytes/string",
Found: args[0].TypeName(),
}
}
}
func jsonHTMLEscape(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
switch o := args[0].(type) {
case *objects.Bytes:
var dst bytes.Buffer
gojson.HTMLEscape(&dst, o.Value)
return &objects.Bytes{Value: dst.Bytes()}, nil
case *objects.String:
var dst bytes.Buffer
gojson.HTMLEscape(&dst, []byte(o.Value))
return &objects.Bytes{Value: dst.Bytes()}, nil
default:
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "bytes/string",
Found: args[0].TypeName(),
}
}
}

374
vendor/github.com/d5/tengo/stdlib/json/decode.go generated vendored Normal file
View File

@ -0,0 +1,374 @@
// A modified version of Go's JSON implementation.
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"strconv"
"unicode"
"unicode/utf16"
"unicode/utf8"
"github.com/d5/tengo/objects"
)
// Decode parses the JSON-encoded data and returns the result object.
func Decode(data []byte) (objects.Object, error) {
var d decodeState
err := checkValid(data, &d.scan)
if err != nil {
return nil, err
}
d.init(data)
d.scan.reset()
d.scanWhile(scanSkipSpace)
return d.value()
}
// decodeState represents the state while decoding a JSON value.
type decodeState struct {
data []byte
off int // next read offset in data
opcode int // last read result
scan scanner
}
// readIndex returns the position of the last byte read.
func (d *decodeState) readIndex() int {
return d.off - 1
}
const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?"
func (d *decodeState) init(data []byte) *decodeState {
d.data = data
d.off = 0
return d
}
// scanNext processes the byte at d.data[d.off].
func (d *decodeState) scanNext() {
if d.off < len(d.data) {
d.opcode = d.scan.step(&d.scan, d.data[d.off])
d.off++
} else {
d.opcode = d.scan.eof()
d.off = len(d.data) + 1 // mark processed EOF with len+1
}
}
// scanWhile processes bytes in d.data[d.off:] until it
// receives a scan code not equal to op.
func (d *decodeState) scanWhile(op int) {
s, data, i := &d.scan, d.data, d.off
for i < len(data) {
newOp := s.step(s, data[i])
i++
if newOp != op {
d.opcode = newOp
d.off = i
return
}
}
d.off = len(data) + 1 // mark processed EOF with len+1
d.opcode = d.scan.eof()
}
func (d *decodeState) value() (objects.Object, error) {
switch d.opcode {
default:
panic(phasePanicMsg)
case scanBeginArray:
o, err := d.array()
if err != nil {
return nil, err
}
d.scanNext()
return o, nil
case scanBeginObject:
o, err := d.object()
if err != nil {
return nil, err
}
d.scanNext()
return o, nil
case scanBeginLiteral:
return d.literal()
}
}
func (d *decodeState) array() (objects.Object, error) {
var arr []objects.Object
for {
// Look ahead for ] - can only happen on first iteration.
d.scanWhile(scanSkipSpace)
if d.opcode == scanEndArray {
break
}
o, err := d.value()
if err != nil {
return nil, err
}
arr = append(arr, o)
// Next token must be , or ].
if d.opcode == scanSkipSpace {
d.scanWhile(scanSkipSpace)
}
if d.opcode == scanEndArray {
break
}
if d.opcode != scanArrayValue {
panic(phasePanicMsg)
}
}
return &objects.Array{Value: arr}, nil
}
func (d *decodeState) object() (objects.Object, error) {
m := make(map[string]objects.Object)
for {
// Read opening " of string key or closing }.
d.scanWhile(scanSkipSpace)
if d.opcode == scanEndObject {
// closing } - can only happen on first iteration.
break
}
if d.opcode != scanBeginLiteral {
panic(phasePanicMsg)
}
// Read string key.
start := d.readIndex()
d.scanWhile(scanContinue)
item := d.data[start:d.readIndex()]
key, ok := unquote(item)
if !ok {
panic(phasePanicMsg)
}
// Read : before value.
if d.opcode == scanSkipSpace {
d.scanWhile(scanSkipSpace)
}
if d.opcode != scanObjectKey {
panic(phasePanicMsg)
}
d.scanWhile(scanSkipSpace)
// Read value.
o, err := d.value()
if err != nil {
return nil, err
}
m[key] = o
// Next token must be , or }.
if d.opcode == scanSkipSpace {
d.scanWhile(scanSkipSpace)
}
if d.opcode == scanEndObject {
break
}
if d.opcode != scanObjectValue {
panic(phasePanicMsg)
}
}
return &objects.Map{Value: m}, nil
}
func (d *decodeState) literal() (objects.Object, error) {
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.scanWhile(scanContinue)
item := d.data[start:d.readIndex()]
switch c := item[0]; c {
case 'n': // null
return objects.UndefinedValue, nil
case 't', 'f': // true, false
if c == 't' {
return objects.TrueValue, nil
}
return objects.FalseValue, nil
case '"': // string
s, ok := unquote(item)
if !ok {
panic(phasePanicMsg)
}
return &objects.String{Value: s}, nil
default: // number
if c != '-' && (c < '0' || c > '9') {
panic(phasePanicMsg)
}
n, _ := strconv.ParseFloat(string(item), 10)
return &objects.Float{Value: n}, nil
}
}
// getu4 decodes \uXXXX from the beginning of s, returning the hex value,
// or it returns -1.
func getu4(s []byte) rune {
if len(s) < 6 || s[0] != '\\' || s[1] != 'u' {
return -1
}
var r rune
for _, c := range s[2:6] {
switch {
case '0' <= c && c <= '9':
c = c - '0'
case 'a' <= c && c <= 'f':
c = c - 'a' + 10
case 'A' <= c && c <= 'F':
c = c - 'A' + 10
default:
return -1
}
r = r*16 + rune(c)
}
return r
}
// unquote converts a quoted JSON string literal s into an actual string t.
// The rules are different than for Go, so cannot use strconv.Unquote.
func unquote(s []byte) (t string, ok bool) {
s, ok = unquoteBytes(s)
t = string(s)
return
}
func unquoteBytes(s []byte) (t []byte, ok bool) {
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
return
}
s = s[1 : len(s)-1]
// Check for unusual characters. If there are none,
// then no unquoting is needed, so return a slice of the
// original bytes.
r := 0
for r < len(s) {
c := s[r]
if c == '\\' || c == '"' || c < ' ' {
break
}
if c < utf8.RuneSelf {
r++
continue
}
rr, size := utf8.DecodeRune(s[r:])
if rr == utf8.RuneError && size == 1 {
break
}
r += size
}
if r == len(s) {
return s, true
}
b := make([]byte, len(s)+2*utf8.UTFMax)
w := copy(b, s[0:r])
for r < len(s) {
// Out of room? Can only happen if s is full of
// malformed UTF-8 and we're replacing each
// byte with RuneError.
if w >= len(b)-2*utf8.UTFMax {
nb := make([]byte, (len(b)+utf8.UTFMax)*2)
copy(nb, b[0:w])
b = nb
}
switch c := s[r]; {
case c == '\\':
r++
if r >= len(s) {
return
}
switch s[r] {
default:
return
case '"', '\\', '/', '\'':
b[w] = s[r]
r++
w++
case 'b':
b[w] = '\b'
r++
w++
case 'f':
b[w] = '\f'
r++
w++
case 'n':
b[w] = '\n'
r++
w++
case 'r':
b[w] = '\r'
r++
w++
case 't':
b[w] = '\t'
r++
w++
case 'u':
r--
rr := getu4(s[r:])
if rr < 0 {
return
}
r += 6
if utf16.IsSurrogate(rr) {
rr1 := getu4(s[r:])
if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar {
// A valid pair; consume.
r += 6
w += utf8.EncodeRune(b[w:], dec)
break
}
// Invalid surrogate; fall back to replacement rune.
rr = unicode.ReplacementChar
}
w += utf8.EncodeRune(b[w:], rr)
}
// Quote, control characters are invalid.
case c == '"', c < ' ':
return
// ASCII
case c < utf8.RuneSelf:
b[w] = c
r++
w++
// Coerce to well-formed UTF-8.
default:
rr, size := utf8.DecodeRune(s[r:])
r += size
w += utf8.EncodeRune(b[w:], rr)
}
}
return b[0:w], true
}

147
vendor/github.com/d5/tengo/stdlib/json/encode.go generated vendored Normal file
View File

@ -0,0 +1,147 @@
// A modified version of Go's JSON implementation.
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import (
"encoding/base64"
"errors"
"math"
"strconv"
"github.com/d5/tengo/objects"
)
// Encode returns the JSON encoding of the object.
func Encode(o objects.Object) ([]byte, error) {
var b []byte
switch o := o.(type) {
case *objects.Array:
b = append(b, '[')
len1 := len(o.Value) - 1
for idx, elem := range o.Value {
eb, err := Encode(elem)
if err != nil {
return nil, err
}
b = append(b, eb...)
if idx < len1 {
b = append(b, ',')
}
}
b = append(b, ']')
case *objects.ImmutableArray:
b = append(b, '[')
len1 := len(o.Value) - 1
for idx, elem := range o.Value {
eb, err := Encode(elem)
if err != nil {
return nil, err
}
b = append(b, eb...)
if idx < len1 {
b = append(b, ',')
}
}
b = append(b, ']')
case *objects.Map:
b = append(b, '{')
len1 := len(o.Value) - 1
idx := 0
for key, value := range o.Value {
b = strconv.AppendQuote(b, key)
b = append(b, ':')
eb, err := Encode(value)
if err != nil {
return nil, err
}
b = append(b, eb...)
if idx < len1 {
b = append(b, ',')
}
idx++
}
b = append(b, '}')
case *objects.ImmutableMap:
b = append(b, '{')
len1 := len(o.Value) - 1
idx := 0
for key, value := range o.Value {
b = strconv.AppendQuote(b, key)
b = append(b, ':')
eb, err := Encode(value)
if err != nil {
return nil, err
}
b = append(b, eb...)
if idx < len1 {
b = append(b, ',')
}
idx++
}
b = append(b, '}')
case *objects.Bool:
if o.IsFalsy() {
b = strconv.AppendBool(b, false)
} else {
b = strconv.AppendBool(b, true)
}
case *objects.Bytes:
b = append(b, '"')
encodedLen := base64.StdEncoding.EncodedLen(len(o.Value))
dst := make([]byte, encodedLen)
base64.StdEncoding.Encode(dst, o.Value)
b = append(b, dst...)
b = append(b, '"')
case *objects.Char:
b = strconv.AppendInt(b, int64(o.Value), 10)
case *objects.Float:
var y []byte
f := o.Value
if math.IsInf(f, 0) || math.IsNaN(f) {
return nil, errors.New("unsupported float value")
}
// Convert as if by ES6 number to string conversion.
// This matches most other JSON generators.
abs := math.Abs(f)
fmt := byte('f')
if abs != 0 {
if abs < 1e-6 || abs >= 1e21 {
fmt = 'e'
}
}
y = strconv.AppendFloat(y, f, fmt, -1, 64)
if fmt == 'e' {
// clean up e-09 to e-9
n := len(y)
if n >= 4 && y[n-4] == 'e' && y[n-3] == '-' && y[n-2] == '0' {
y[n-2] = y[n-1]
y = y[:n-1]
}
}
b = append(b, y...)
case *objects.Int:
b = strconv.AppendInt(b, o.Value, 10)
case *objects.String:
b = strconv.AppendQuote(b, o.Value)
case *objects.Time:
y, err := o.Value.MarshalJSON()
if err != nil {
return nil, err
}
b = append(b, y...)
case *objects.Undefined:
b = append(b, "null"...)
default:
// unknown type: ignore
}
return b, nil
}

559
vendor/github.com/d5/tengo/stdlib/json/scanner.go generated vendored Normal file
View File

@ -0,0 +1,559 @@
// A modified version of Go's JSON implementation.
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package json
import "strconv"
func checkValid(data []byte, scan *scanner) error {
scan.reset()
for _, c := range data {
scan.bytes++
if scan.step(scan, c) == scanError {
return scan.err
}
}
if scan.eof() == scanError {
return scan.err
}
return nil
}
// A SyntaxError is a description of a JSON syntax error.
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
// A scanner is a JSON scanning state machine.
// Callers call scan.reset() and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in. (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
// The step is a func to be called to execute the next transition.
// Also tried using an integer constant and a single func
// with a switch, but using the func directly was 10% faster
// on a 64-bit Mac Mini, and it's nicer to read.
step func(*scanner, byte) int
// Reached end of top-level value.
endTop bool
// Stack of what we're in the middle of - array values, object keys, object values.
parseState []int
// Error that happened, if any.
err error
// total bytes consumed, updated by decoder.Decode
bytes int64
}
// These values are returned by the state transition functions
// assigned to scanner.state and the method scanner.eof.
// They give details about the current state of the scan that
// callers might be interested to know about.
// It is okay to ignore the return value of any particular
// call to scanner.state: if one call returns scanError,
// every subsequent call will return scanError too.
const (
// Continue.
scanContinue = iota // uninteresting byte
scanBeginLiteral // end implied by next result != scanContinue
scanBeginObject // begin object
scanObjectKey // just finished object key (string)
scanObjectValue // just finished non-last object value
scanEndObject // end object (implies scanObjectValue if possible)
scanBeginArray // begin array
scanArrayValue // just finished array value
scanEndArray // end array (implies scanArrayValue if possible)
scanSkipSpace // space byte; can skip; known to be last "continue" result
// Stop.
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
scanError // hit an error, scanner.err.
)
// These values are stored in the parseState stack.
// They give the current state of a composite value
// being scanned. If the parser is inside a nested value
// the parseState describes the nested state, outermost at entry 0.
const (
parseObjectKey = iota // parsing object key (before colon)
parseObjectValue // parsing object value (after colon)
parseArrayValue // parsing array value
)
// reset prepares the scanner for use.
// It must be called before calling s.step.
func (s *scanner) reset() {
s.step = stateBeginValue
s.parseState = s.parseState[0:0]
s.err = nil
s.endTop = false
}
// eof tells the scanner that the end of input has been reached.
// It returns a scan status just as s.step does.
func (s *scanner) eof() int {
if s.err != nil {
return scanError
}
if s.endTop {
return scanEnd
}
s.step(s, ' ')
if s.endTop {
return scanEnd
}
if s.err == nil {
s.err = &SyntaxError{"unexpected end of JSON input", s.bytes}
}
return scanError
}
// pushParseState pushes a new parse state p onto the parse stack.
func (s *scanner) pushParseState(p int) {
s.parseState = append(s.parseState, p)
}
// popParseState pops a parse state (already obtained) off the stack
// and updates s.step accordingly.
func (s *scanner) popParseState() {
n := len(s.parseState) - 1
s.parseState = s.parseState[0:n]
if n == 0 {
s.step = stateEndTop
s.endTop = true
} else {
s.step = stateEndValue
}
}
func isSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
// stateBeginValueOrEmpty is the state after reading `[`.
func stateBeginValueOrEmpty(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == ']' {
return stateEndValue(s, c)
}
return stateBeginValue(s, c)
}
// stateBeginValue is the state at the beginning of the input.
func stateBeginValue(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
switch c {
case '{':
s.step = stateBeginStringOrEmpty
s.pushParseState(parseObjectKey)
return scanBeginObject
case '[':
s.step = stateBeginValueOrEmpty
s.pushParseState(parseArrayValue)
return scanBeginArray
case '"':
s.step = stateInString
return scanBeginLiteral
case '-':
s.step = stateNeg
return scanBeginLiteral
case '0': // beginning of 0.123
s.step = state0
return scanBeginLiteral
case 't': // beginning of true
s.step = stateT
return scanBeginLiteral
case 'f': // beginning of false
s.step = stateF
return scanBeginLiteral
case 'n': // beginning of null
s.step = stateN
return scanBeginLiteral
}
if '1' <= c && c <= '9' { // beginning of 1234.5
s.step = state1
return scanBeginLiteral
}
return s.error(c, "looking for beginning of value")
}
// stateBeginStringOrEmpty is the state after reading `{`.
func stateBeginStringOrEmpty(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == '}' {
n := len(s.parseState)
s.parseState[n-1] = parseObjectValue
return stateEndValue(s, c)
}
return stateBeginString(s, c)
}
// stateBeginString is the state after reading `{"key": value,`.
func stateBeginString(s *scanner, c byte) int {
if c <= ' ' && isSpace(c) {
return scanSkipSpace
}
if c == '"' {
s.step = stateInString
return scanBeginLiteral
}
return s.error(c, "looking for beginning of object key string")
}
// stateEndValue is the state after completing a value,
// such as after reading `{}` or `true` or `["x"`.
func stateEndValue(s *scanner, c byte) int {
n := len(s.parseState)
if n == 0 {
// Completed top-level before the current byte.
s.step = stateEndTop
s.endTop = true
return stateEndTop(s, c)
}
if c <= ' ' && isSpace(c) {
s.step = stateEndValue
return scanSkipSpace
}
ps := s.parseState[n-1]
switch ps {
case parseObjectKey:
if c == ':' {
s.parseState[n-1] = parseObjectValue
s.step = stateBeginValue
return scanObjectKey
}
return s.error(c, "after object key")
case parseObjectValue:
if c == ',' {
s.parseState[n-1] = parseObjectKey
s.step = stateBeginString
return scanObjectValue
}
if c == '}' {
s.popParseState()
return scanEndObject
}
return s.error(c, "after object key:value pair")
case parseArrayValue:
if c == ',' {
s.step = stateBeginValue
return scanArrayValue
}
if c == ']' {
s.popParseState()
return scanEndArray
}
return s.error(c, "after array element")
}
return s.error(c, "")
}
// stateEndTop is the state after finishing the top-level value,
// such as after reading `{}` or `[1,2,3]`.
// Only space characters should be seen now.
func stateEndTop(s *scanner, c byte) int {
if !isSpace(c) {
// Complain about non-space byte on next call.
s.error(c, "after top-level value")
}
return scanEnd
}
// stateInString is the state after reading `"`.
func stateInString(s *scanner, c byte) int {
if c == '"' {
s.step = stateEndValue
return scanContinue
}
if c == '\\' {
s.step = stateInStringEsc
return scanContinue
}
if c < 0x20 {
return s.error(c, "in string literal")
}
return scanContinue
}
// stateInStringEsc is the state after reading `"\` during a quoted string.
func stateInStringEsc(s *scanner, c byte) int {
switch c {
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
s.step = stateInString
return scanContinue
case 'u':
s.step = stateInStringEscU
return scanContinue
}
return s.error(c, "in string escape code")
}
// stateInStringEscU is the state after reading `"\u` during a quoted string.
func stateInStringEscU(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU1
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU1 is the state after reading `"\u1` during a quoted string.
func stateInStringEscU1(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU12
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU12 is the state after reading `"\u12` during a quoted string.
func stateInStringEscU12(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInStringEscU123
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateInStringEscU123 is the state after reading `"\u123` during a quoted string.
func stateInStringEscU123(s *scanner, c byte) int {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
s.step = stateInString
return scanContinue
}
// numbers
return s.error(c, "in \\u hexadecimal character escape")
}
// stateNeg is the state after reading `-` during a number.
func stateNeg(s *scanner, c byte) int {
if c == '0' {
s.step = state0
return scanContinue
}
if '1' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return s.error(c, "in numeric literal")
}
// state1 is the state after reading a non-zero integer during a number,
// such as after reading `1` or `100` but not `0`.
func state1(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = state1
return scanContinue
}
return state0(s, c)
}
// state0 is the state after reading `0` during a number.
func state0(s *scanner, c byte) int {
if c == '.' {
s.step = stateDot
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateDot is the state after reading the integer and decimal point in a number,
// such as after reading `1.`.
func stateDot(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateDot0
return scanContinue
}
return s.error(c, "after decimal point in numeric literal")
}
// stateDot0 is the state after reading the integer, decimal point, and subsequent
// digits of a number, such as after reading `3.14`.
func stateDot0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
if c == 'e' || c == 'E' {
s.step = stateE
return scanContinue
}
return stateEndValue(s, c)
}
// stateE is the state after reading the mantissa and e in a number,
// such as after reading `314e` or `0.314e`.
func stateE(s *scanner, c byte) int {
if c == '+' || c == '-' {
s.step = stateESign
return scanContinue
}
return stateESign(s, c)
}
// stateESign is the state after reading the mantissa, e, and sign in a number,
// such as after reading `314e-` or `0.314e+`.
func stateESign(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
s.step = stateE0
return scanContinue
}
return s.error(c, "in exponent of numeric literal")
}
// stateE0 is the state after reading the mantissa, e, optional sign,
// and at least one digit of the exponent in a number,
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
func stateE0(s *scanner, c byte) int {
if '0' <= c && c <= '9' {
return scanContinue
}
return stateEndValue(s, c)
}
// stateT is the state after reading `t`.
func stateT(s *scanner, c byte) int {
if c == 'r' {
s.step = stateTr
return scanContinue
}
return s.error(c, "in literal true (expecting 'r')")
}
// stateTr is the state after reading `tr`.
func stateTr(s *scanner, c byte) int {
if c == 'u' {
s.step = stateTru
return scanContinue
}
return s.error(c, "in literal true (expecting 'u')")
}
// stateTru is the state after reading `tru`.
func stateTru(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal true (expecting 'e')")
}
// stateF is the state after reading `f`.
func stateF(s *scanner, c byte) int {
if c == 'a' {
s.step = stateFa
return scanContinue
}
return s.error(c, "in literal false (expecting 'a')")
}
// stateFa is the state after reading `fa`.
func stateFa(s *scanner, c byte) int {
if c == 'l' {
s.step = stateFal
return scanContinue
}
return s.error(c, "in literal false (expecting 'l')")
}
// stateFal is the state after reading `fal`.
func stateFal(s *scanner, c byte) int {
if c == 's' {
s.step = stateFals
return scanContinue
}
return s.error(c, "in literal false (expecting 's')")
}
// stateFals is the state after reading `fals`.
func stateFals(s *scanner, c byte) int {
if c == 'e' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal false (expecting 'e')")
}
// stateN is the state after reading `n`.
func stateN(s *scanner, c byte) int {
if c == 'u' {
s.step = stateNu
return scanContinue
}
return s.error(c, "in literal null (expecting 'u')")
}
// stateNu is the state after reading `nu`.
func stateNu(s *scanner, c byte) int {
if c == 'l' {
s.step = stateNul
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateNul is the state after reading `nul`.
func stateNul(s *scanner, c byte) int {
if c == 'l' {
s.step = stateEndValue
return scanContinue
}
return s.error(c, "in literal null (expecting 'l')")
}
// stateError is the state after reaching a syntax error,
// such as after reading `[1}` or `5.1.2`.
func stateError(s *scanner, c byte) int {
return scanError
}
// error records an error and switches to the error state.
func (s *scanner) error(c byte, context string) int {
s.step = stateError
s.err = &SyntaxError{"invalid character " + quoteChar(c) + " " + context, s.bytes}
return scanError
}
// quoteChar formats c as a quoted character literal
func quoteChar(c byte) string {
// special cases - different from quoted strings
if c == '\'' {
return `'\''`
}
if c == '"' {
return `'"'`
}
// use quoted string with different quotation marks
s := strconv.Quote(string(c))
return "'" + s[1:len(s)-1] + "'"
}

View File

@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -39,14 +40,14 @@ var osModule = map[string]objects.Object{
"seek_set": &objects.Int{Value: int64(io.SeekStart)},
"seek_cur": &objects.Int{Value: int64(io.SeekCurrent)},
"seek_end": &objects.Int{Value: int64(io.SeekEnd)},
"args": &objects.UserFunction{Value: osArgs}, // args() => array(string)
"args": &objects.UserFunction{Name: "args", Value: osArgs}, // args() => array(string)
"chdir": &objects.UserFunction{Name: "chdir", Value: FuncASRE(os.Chdir)}, // chdir(dir string) => error
"chmod": osFuncASFmRE(os.Chmod), // chmod(name string, mode int) => error
"chmod": osFuncASFmRE("chmod", os.Chmod), // chmod(name string, mode int) => error
"chown": &objects.UserFunction{Name: "chown", Value: FuncASIIRE(os.Chown)}, // chown(name string, uid int, gid int) => error
"clearenv": &objects.UserFunction{Name: "clearenv", Value: FuncAR(os.Clearenv)}, // clearenv()
"environ": &objects.UserFunction{Name: "environ", Value: FuncARSs(os.Environ)}, // environ() => array(string)
"exit": &objects.UserFunction{Name: "exit", Value: FuncAIR(os.Exit)}, // exit(code int)
"expand_env": &objects.UserFunction{Name: "expand_env", Value: FuncASRS(os.ExpandEnv)}, // expand_env(s string) => string
"expand_env": &objects.UserFunction{Name: "expand_env", Value: osExpandEnv}, // expand_env(s string) => string
"getegid": &objects.UserFunction{Name: "getegid", Value: FuncARI(os.Getegid)}, // getegid() => int
"getenv": &objects.UserFunction{Name: "getenv", Value: FuncASRS(os.Getenv)}, // getenv(s string) => string
"geteuid": &objects.UserFunction{Name: "geteuid", Value: FuncARI(os.Geteuid)}, // geteuid() => int
@ -60,9 +61,9 @@ var osModule = map[string]objects.Object{
"hostname": &objects.UserFunction{Name: "hostname", Value: FuncARSE(os.Hostname)}, // hostname() => string/error
"lchown": &objects.UserFunction{Name: "lchown", Value: FuncASIIRE(os.Lchown)}, // lchown(name string, uid int, gid int) => error
"link": &objects.UserFunction{Name: "link", Value: FuncASSRE(os.Link)}, // link(oldname string, newname string) => error
"lookup_env": &objects.UserFunction{Value: osLookupEnv}, // lookup_env(key string) => string/false
"mkdir": osFuncASFmRE(os.Mkdir), // mkdir(name string, perm int) => error
"mkdir_all": osFuncASFmRE(os.MkdirAll), // mkdir_all(name string, perm int) => error
"lookup_env": &objects.UserFunction{Name: "lookup_env", Value: osLookupEnv}, // lookup_env(key string) => string/false
"mkdir": osFuncASFmRE("mkdir", os.Mkdir), // mkdir(name string, perm int) => error
"mkdir_all": osFuncASFmRE("mkdir_all", os.MkdirAll), // mkdir_all(name string, perm int) => error
"readlink": &objects.UserFunction{Name: "readlink", Value: FuncASRSE(os.Readlink)}, // readlink(name string) => string/error
"remove": &objects.UserFunction{Name: "remove", Value: FuncASRE(os.Remove)}, // remove(name string) => error
"remove_all": &objects.UserFunction{Name: "remove_all", Value: FuncASRE(os.RemoveAll)}, // remove_all(name string) => error
@ -72,15 +73,15 @@ var osModule = map[string]objects.Object{
"temp_dir": &objects.UserFunction{Name: "temp_dir", Value: FuncARS(os.TempDir)}, // temp_dir() => string
"truncate": &objects.UserFunction{Name: "truncate", Value: FuncASI64RE(os.Truncate)}, // truncate(name string, size int) => error
"unsetenv": &objects.UserFunction{Name: "unsetenv", Value: FuncASRE(os.Unsetenv)}, // unsetenv(key string) => error
"create": &objects.UserFunction{Value: osCreate}, // create(name string) => imap(file)/error
"open": &objects.UserFunction{Value: osOpen}, // open(name string) => imap(file)/error
"open_file": &objects.UserFunction{Value: osOpenFile}, // open_file(name string, flag int, perm int) => imap(file)/error
"find_process": &objects.UserFunction{Value: osFindProcess}, // find_process(pid int) => imap(process)/error
"start_process": &objects.UserFunction{Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error
"create": &objects.UserFunction{Name: "create", Value: osCreate}, // create(name string) => imap(file)/error
"open": &objects.UserFunction{Name: "open", Value: osOpen}, // open(name string) => imap(file)/error
"open_file": &objects.UserFunction{Name: "open_file", Value: osOpenFile}, // open_file(name string, flag int, perm int) => imap(file)/error
"find_process": &objects.UserFunction{Name: "find_process", Value: osFindProcess}, // find_process(pid int) => imap(process)/error
"start_process": &objects.UserFunction{Name: "start_process", Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error
"exec_look_path": &objects.UserFunction{Name: "exec_look_path", Value: FuncASRSE(exec.LookPath)}, // exec_look_path(file) => string/error
"exec": &objects.UserFunction{Value: osExec}, // exec(name, args...) => command
"stat": &objects.UserFunction{Value: osStat}, // stat(name) => imap(fileinfo)/error
"read_file": &objects.UserFunction{Value: osReadFile}, // readfile(name) => array(byte)/error
"exec": &objects.UserFunction{Name: "exec", Value: osExec}, // exec(name, args...) => command
"stat": &objects.UserFunction{Name: "stat", Value: osStat}, // stat(name) => imap(fileinfo)/error
"read_file": &objects.UserFunction{Name: "read_file", Value: osReadFile}, // readfile(name) => array(byte)/error
}
func osReadFile(args ...objects.Object) (ret objects.Object, err error) {
@ -102,6 +103,10 @@ func osReadFile(args ...objects.Object) (ret objects.Object, err error) {
return wrapError(err), nil
}
if len(bytes) > tengo.MaxBytesLen {
return nil, objects.ErrBytesLimit
}
return &objects.Bytes{Value: bytes}, nil
}
@ -233,14 +238,19 @@ func osArgs(args ...objects.Object) (objects.Object, error) {
arr := &objects.Array{}
for _, osArg := range os.Args {
if len(osArg) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
arr.Value = append(arr.Value, &objects.String{Value: osArg})
}
return arr, nil
}
func osFuncASFmRE(fn func(string, os.FileMode) error) *objects.UserFunction {
func osFuncASFmRE(name string, fn func(string, os.FileMode) error) *objects.UserFunction {
return &objects.UserFunction{
Name: name,
Value: func(args ...objects.Object) (objects.Object, error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
@ -287,9 +297,54 @@ func osLookupEnv(args ...objects.Object) (objects.Object, error) {
return objects.FalseValue, nil
}
if len(res) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: res}, nil
}
func osExpandEnv(args ...objects.Object) (objects.Object, error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
}
s1, ok := objects.ToString(args[0])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "string(compatible)",
Found: args[0].TypeName(),
}
}
var vlen int
var failed bool
s := os.Expand(s1, func(k string) string {
if failed {
return ""
}
v := os.Getenv(k)
// this does not count the other texts that are not being replaced
// but the code checks the final length at the end
vlen += len(v)
if vlen > tengo.MaxStringLen {
failed = true
return ""
}
return v
})
if failed || len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: s}, nil
}
func osExec(args ...objects.Object) (objects.Object, error) {
if len(args) == 0 {
return nil, objects.ErrWrongNumArguments

View File

@ -21,6 +21,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
"wait": &objects.UserFunction{Name: "wait", Value: FuncARE(cmd.Wait)}, //
// set_path(path string)
"set_path": &objects.UserFunction{
Name: "set_path",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -42,6 +43,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// set_dir(dir string)
"set_dir": &objects.UserFunction{
Name: "set_dir",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -63,6 +65,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// set_env(env array(string))
"set_env": &objects.UserFunction{
Name: "set_env",
Value: func(args ...objects.Object) (objects.Object, error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -96,6 +99,7 @@ func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap {
},
// process() => imap(process)
"process": &objects.UserFunction{
Name: "process",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View File

@ -29,6 +29,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
"read": &objects.UserFunction{Name: "read", Value: FuncAYRIE(file.Read)}, //
// chmod(mode int) => error
"chmod": &objects.UserFunction{
Name: "chmod",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -48,6 +49,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
},
// seek(offset int, whence int) => int/error
"seek": &objects.UserFunction{
Name: "seek",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
@ -80,6 +82,7 @@ func makeOSFile(file *os.File) *objects.ImmutableMap {
},
// stat() => imap(fileinfo)/error
"stat": &objects.UserFunction{
Name: "start",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View File

@ -24,6 +24,7 @@ func makeOSProcess(proc *os.Process) *objects.ImmutableMap {
"kill": &objects.UserFunction{Name: "kill", Value: FuncARE(proc.Kill)}, //
"release": &objects.UserFunction{Name: "release", Value: FuncARE(proc.Release)}, //
"signal": &objects.UserFunction{
Name: "signal",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -42,6 +43,7 @@ func makeOSProcess(proc *os.Process) *objects.ImmutableMap {
},
},
"wait": &objects.UserFunction{
Name: "wait",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 0 {
return nil, objects.ErrWrongNumArguments

View File

@ -15,6 +15,7 @@ var randModule = map[string]objects.Object{
"perm": &objects.UserFunction{Name: "perm", Value: FuncAIRIs(rand.Perm)},
"seed": &objects.UserFunction{Name: "seed", Value: FuncAI64R(rand.Seed)},
"read": &objects.UserFunction{
Name: "read",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -39,6 +40,7 @@ var randModule = map[string]objects.Object{
},
},
"rand": &objects.UserFunction{
Name: "rand",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments
@ -71,6 +73,7 @@ func randRand(r *rand.Rand) *objects.ImmutableMap {
"perm": &objects.UserFunction{Name: "perm", Value: FuncAIRIs(r.Perm)},
"seed": &objects.UserFunction{Name: "seed", Value: FuncAI64R(r.Seed)},
"read": &objects.UserFunction{
Name: "read",
Value: func(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
return nil, objects.ErrWrongNumArguments

8
vendor/github.com/d5/tengo/stdlib/source_modules.go generated vendored Normal file
View File

@ -0,0 +1,8 @@
// Code generated using gensrcmods.go; DO NOT EDIT.
package stdlib
// SourceModules are source type standard library modules.
var SourceModules = map[string]string{
"enum": "is_enumerable := func(x) {\n return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x)\n}\n\nis_array_like := func(x) {\n return is_array(x) || is_immutable_array(x)\n}\n\nexport {\n // all returns true if the given function `fn` evaluates to a truthy value on\n // all of the items in `x`. It returns undefined if `x` is not enumerable.\n all: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if !fn(k, v) { return false }\n }\n\n return true\n },\n // any returns true if the given function `fn` evaluates to a truthy value on\n // any of the items in `x`. It returns undefined if `x` is not enumerable.\n any: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return true }\n }\n\n return false\n },\n // chunk returns an array of elements split into groups the length of size.\n // If `x` can't be split evenly, the final chunk will be the remaining elements.\n // It returns undefined if `x` is not array.\n chunk: func(x, size) {\n if !is_array_like(x) || !size { return undefined }\n\n numElements := len(x)\n if !numElements { return [] }\n\n res := []\n idx := 0\n for idx < numElements {\n res = append(res, x[idx:idx+size])\n idx += size\n }\n\n return res\n },\n // at returns an element at the given index (if `x` is array) or\n // key (if `x` is map). It returns undefined if `x` is not enumerable.\n at: func(x, key) {\n if !is_enumerable(x) { return undefined }\n\n if is_array_like(x) {\n if !is_int(key) { return undefined }\n } else {\n if !is_string(key) { return undefined }\n }\n\n return x[key]\n },\n // each iterates over elements of `x` and invokes `fn` for each element. `fn` is\n // invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It does not iterate\n // and returns undefined if `x` is not enumerable.\n each: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n fn(k, v)\n }\n },\n // filter iterates over elements of `x`, returning an array of all elements `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. `key` is a string key if `x` is map.\n // It returns undefined if `x` is not enumerable.\n filter: func(x, fn) {\n if !is_array_like(x) { return undefined }\n\n dst := []\n for k, v in x {\n if fn(k, v) { dst = append(dst, v) }\n }\n\n return dst\n },\n // find iterates over elements of `x`, returning value of the first element `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. `key` is a string key if `x` is map.\n // It returns undefined if `x` is not enumerable.\n find: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return v }\n }\n },\n // find_key iterates over elements of `x`, returning key or index of the first\n // element `fn` returns truthy for. `fn` is invoked with two arguments: `key`\n // and `value`. `key` is an int index if `x` is array. `key` is a string key if\n // `x` is map. It returns undefined if `x` is not enumerable.\n find_key: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return k }\n }\n },\n // map creates an array of values by running each element in `x` through `fn`.\n // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It returns undefined\n // if `x` is not enumerable.\n map: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n dst := []\n for k, v in x {\n dst = append(dst, fn(k, v))\n }\n\n return dst\n },\n // key returns the first argument.\n key: func(k, _) { return k },\n // value returns the second argument.\n value: func(_, v) { return v }\n}\n",
}

128
vendor/github.com/d5/tengo/stdlib/srcmod_enum.tengo generated vendored Normal file
View File

@ -0,0 +1,128 @@
is_enumerable := func(x) {
return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x)
}
is_array_like := func(x) {
return is_array(x) || is_immutable_array(x)
}
export {
// all returns true if the given function `fn` evaluates to a truthy value on
// all of the items in `x`. It returns undefined if `x` is not enumerable.
all: func(x, fn) {
if !is_enumerable(x) { return undefined }
for k, v in x {
if !fn(k, v) { return false }
}
return true
},
// any returns true if the given function `fn` evaluates to a truthy value on
// any of the items in `x`. It returns undefined if `x` is not enumerable.
any: func(x, fn) {
if !is_enumerable(x) { return undefined }
for k, v in x {
if fn(k, v) { return true }
}
return false
},
// chunk returns an array of elements split into groups the length of size.
// If `x` can't be split evenly, the final chunk will be the remaining elements.
// It returns undefined if `x` is not array.
chunk: func(x, size) {
if !is_array_like(x) || !size { return undefined }
numElements := len(x)
if !numElements { return [] }
res := []
idx := 0
for idx < numElements {
res = append(res, x[idx:idx+size])
idx += size
}
return res
},
// at returns an element at the given index (if `x` is array) or
// key (if `x` is map). It returns undefined if `x` is not enumerable.
at: func(x, key) {
if !is_enumerable(x) { return undefined }
if is_array_like(x) {
if !is_int(key) { return undefined }
} else {
if !is_string(key) { return undefined }
}
return x[key]
},
// each iterates over elements of `x` and invokes `fn` for each element. `fn` is
// invoked with two arguments: `key` and `value`. `key` is an int index
// if `x` is array. `key` is a string key if `x` is map. It does not iterate
// and returns undefined if `x` is not enumerable.
each: func(x, fn) {
if !is_enumerable(x) { return undefined }
for k, v in x {
fn(k, v)
}
},
// filter iterates over elements of `x`, returning an array of all elements `fn`
// returns truthy for. `fn` is invoked with two arguments: `key` and `value`.
// `key` is an int index if `x` is array. `key` is a string key if `x` is map.
// It returns undefined if `x` is not enumerable.
filter: func(x, fn) {
if !is_array_like(x) { return undefined }
dst := []
for k, v in x {
if fn(k, v) { dst = append(dst, v) }
}
return dst
},
// find iterates over elements of `x`, returning value of the first element `fn`
// returns truthy for. `fn` is invoked with two arguments: `key` and `value`.
// `key` is an int index if `x` is array. `key` is a string key if `x` is map.
// It returns undefined if `x` is not enumerable.
find: func(x, fn) {
if !is_enumerable(x) { return undefined }
for k, v in x {
if fn(k, v) { return v }
}
},
// find_key iterates over elements of `x`, returning key or index of the first
// element `fn` returns truthy for. `fn` is invoked with two arguments: `key`
// and `value`. `key` is an int index if `x` is array. `key` is a string key if
// `x` is map. It returns undefined if `x` is not enumerable.
find_key: func(x, fn) {
if !is_enumerable(x) { return undefined }
for k, v in x {
if fn(k, v) { return k }
}
},
// map creates an array of values by running each element in `x` through `fn`.
// `fn` is invoked with two arguments: `key` and `value`. `key` is an int index
// if `x` is array. `key` is a string key if `x` is map. It returns undefined
// if `x` is not enumerable.
map: func(x, fn) {
if !is_enumerable(x) { return undefined }
dst := []
for k, v in x {
dst = append(dst, fn(k, v))
}
return dst
},
// key returns the first argument.
key: func(k, _) { return k },
// value returns the second argument.
value: func(_, v) { return v }
}

View File

@ -1,16 +1,34 @@
package stdlib
//go:generate go run gensrcmods.go
import "github.com/d5/tengo/objects"
// Modules contain the standard modules.
var Modules = map[string]*objects.Object{
"math": objectPtr(&objects.ImmutableMap{Value: mathModule}),
"os": objectPtr(&objects.ImmutableMap{Value: osModule}),
"text": objectPtr(&objects.ImmutableMap{Value: textModule}),
"times": objectPtr(&objects.ImmutableMap{Value: timesModule}),
"rand": objectPtr(&objects.ImmutableMap{Value: randModule}),
// AllModuleNames returns a list of all default module names.
func AllModuleNames() []string {
var names []string
for name := range BuiltinModules {
names = append(names, name)
}
for name := range SourceModules {
names = append(names, name)
}
return names
}
func objectPtr(o objects.Object) *objects.Object {
return &o
// GetModuleMap returns the module map that includes all modules
// for the given module names.
func GetModuleMap(names ...string) *objects.ModuleMap {
modules := objects.NewModuleMap()
for _, name := range names {
if mod := BuiltinModules[name]; mod != nil {
modules.AddBuiltinModule(name, mod)
}
if mod := SourceModules[name]; mod != "" {
modules.AddSourceModule(name, []byte(mod))
}
}
return modules
}

View File

@ -1,19 +1,22 @@
package stdlib
import (
"fmt"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
var textModule = map[string]objects.Object{
"re_match": &objects.UserFunction{Value: textREMatch}, // re_match(pattern, text) => bool/error
"re_find": &objects.UserFunction{Value: textREFind}, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined
"re_replace": &objects.UserFunction{Value: textREReplace}, // re_replace(pattern, text, repl) => string/error
"re_split": &objects.UserFunction{Value: textRESplit}, // re_split(pattern, text, count) => [string]/error
"re_compile": &objects.UserFunction{Value: textRECompile}, // re_compile(pattern) => Regexp/error
"re_match": &objects.UserFunction{Name: "re_match", Value: textREMatch}, // re_match(pattern, text) => bool/error
"re_find": &objects.UserFunction{Name: "re_find", Value: textREFind}, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined
"re_replace": &objects.UserFunction{Name: "re_replace", Value: textREReplace}, // re_replace(pattern, text, repl) => string/error
"re_split": &objects.UserFunction{Name: "re_split", Value: textRESplit}, // re_split(pattern, text, count) => [string]/error
"re_compile": &objects.UserFunction{Name: "re_compile", Value: textRECompile}, // re_compile(pattern) => Regexp/error
"compare": &objects.UserFunction{Name: "compare", Value: FuncASSRI(strings.Compare)}, // compare(a, b) => int
"contains": &objects.UserFunction{Name: "contains", Value: FuncASSRB(strings.Contains)}, // contains(s, substr) => bool
"contains_any": &objects.UserFunction{Name: "contains_any", Value: FuncASSRB(strings.ContainsAny)}, // contains_any(s, chars) => bool
@ -24,11 +27,11 @@ var textModule = map[string]objects.Object{
"has_suffix": &objects.UserFunction{Name: "has_suffix", Value: FuncASSRB(strings.HasSuffix)}, // has_suffix(s, suffix) => bool
"index": &objects.UserFunction{Name: "index", Value: FuncASSRI(strings.Index)}, // index(s, substr) => int
"index_any": &objects.UserFunction{Name: "index_any", Value: FuncASSRI(strings.IndexAny)}, // index_any(s, chars) => int
"join": &objects.UserFunction{Name: "join", Value: FuncASsSRS(strings.Join)}, // join(arr, sep) => string
"join": &objects.UserFunction{Name: "join", Value: textJoin}, // join(arr, sep) => string
"last_index": &objects.UserFunction{Name: "last_index", Value: FuncASSRI(strings.LastIndex)}, // last_index(s, substr) => int
"last_index_any": &objects.UserFunction{Name: "last_index_any", Value: FuncASSRI(strings.LastIndexAny)}, // last_index_any(s, chars) => int
"repeat": &objects.UserFunction{Name: "repeat", Value: FuncASIRS(strings.Repeat)}, // repeat(s, count) => string
"replace": &objects.UserFunction{Value: textReplace}, // replace(s, old, new, n) => string
"repeat": &objects.UserFunction{Name: "repeat", Value: textRepeat}, // repeat(s, count) => string
"replace": &objects.UserFunction{Name: "replace", Value: textReplace}, // replace(s, old, new, n) => string
"split": &objects.UserFunction{Name: "split", Value: FuncASSRSs(strings.Split)}, // split(s, sep) => [string]
"split_after": &objects.UserFunction{Name: "split_after", Value: FuncASSRSs(strings.SplitAfter)}, // split_after(s, sep) => [string]
"split_after_n": &objects.UserFunction{Name: "split_after_n", Value: FuncASSIRSs(strings.SplitAfterN)}, // split_after_n(s, sep, n) => [string]
@ -43,13 +46,13 @@ var textModule = map[string]objects.Object{
"trim_space": &objects.UserFunction{Name: "trim_space", Value: FuncASRS(strings.TrimSpace)}, // trim_space(s) => string
"trim_suffix": &objects.UserFunction{Name: "trim_suffix", Value: FuncASSRS(strings.TrimSuffix)}, // trim_suffix(s, suffix) => string
"atoi": &objects.UserFunction{Name: "atoi", Value: FuncASRIE(strconv.Atoi)}, // atoi(str) => int/error
"format_bool": &objects.UserFunction{Value: textFormatBool}, // format_bool(b) => string
"format_float": &objects.UserFunction{Value: textFormatFloat}, // format_float(f, fmt, prec, bits) => string
"format_int": &objects.UserFunction{Value: textFormatInt}, // format_int(i, base) => string
"format_bool": &objects.UserFunction{Name: "format_bool", Value: textFormatBool}, // format_bool(b) => string
"format_float": &objects.UserFunction{Name: "format_float", Value: textFormatFloat}, // format_float(f, fmt, prec, bits) => string
"format_int": &objects.UserFunction{Name: "format_int", Value: textFormatInt}, // format_int(i, base) => string
"itoa": &objects.UserFunction{Name: "itoa", Value: FuncAIRS(strconv.Itoa)}, // itoa(i) => string
"parse_bool": &objects.UserFunction{Value: textParseBool}, // parse_bool(str) => bool/error
"parse_float": &objects.UserFunction{Value: textParseFloat}, // parse_float(str, bits) => float/error
"parse_int": &objects.UserFunction{Value: textParseInt}, // parse_int(str, base, bits) => int/error
"parse_bool": &objects.UserFunction{Name: "parse_bool", Value: textParseBool}, // parse_bool(str) => bool/error
"parse_float": &objects.UserFunction{Name: "parse_float", Value: textParseFloat}, // parse_float(str, bits) => float/error
"parse_int": &objects.UserFunction{Name: "parse_int", Value: textParseInt}, // parse_int(str, base, bits) => int/error
"quote": &objects.UserFunction{Name: "quote", Value: FuncASRS(strconv.Quote)}, // quote(str) => string
"unquote": &objects.UserFunction{Name: "unquote", Value: FuncASRSE(strconv.Unquote)}, // unquote(str) => string/error
}
@ -223,7 +226,12 @@ func textREReplace(args ...objects.Object) (ret objects.Object, err error) {
if err != nil {
ret = wrapError(err)
} else {
ret = &objects.String{Value: re.ReplaceAllString(s2, s3)}
s, ok := doTextRegexpReplace(re, s2, s3)
if !ok {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
}
return
@ -357,11 +365,106 @@ func textReplace(args ...objects.Object) (ret objects.Object, err error) {
return
}
ret = &objects.String{Value: strings.Replace(s1, s2, s3, i4)}
s, ok := doTextReplace(s1, s2, s3, i4)
if !ok {
err = objects.ErrStringLimit
return
}
ret = &objects.String{Value: s}
return
}
func textRepeat(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
}
s1, ok := objects.ToString(args[0])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "string(compatible)",
Found: args[0].TypeName(),
}
}
i2, ok := objects.ToInt(args[1])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "second",
Expected: "int(compatible)",
Found: args[1].TypeName(),
}
}
if len(s1)*i2 > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: strings.Repeat(s1, i2)}, nil
}
func textJoin(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 2 {
return nil, objects.ErrWrongNumArguments
}
var slen int
var ss1 []string
switch arg0 := args[0].(type) {
case *objects.Array:
for idx, a := range arg0.Value {
as, ok := objects.ToString(a)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: fmt.Sprintf("first[%d]", idx),
Expected: "string(compatible)",
Found: a.TypeName(),
}
}
slen += len(as)
ss1 = append(ss1, as)
}
case *objects.ImmutableArray:
for idx, a := range arg0.Value {
as, ok := objects.ToString(a)
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: fmt.Sprintf("first[%d]", idx),
Expected: "string(compatible)",
Found: a.TypeName(),
}
}
slen += len(as)
ss1 = append(ss1, as)
}
default:
return nil, objects.ErrInvalidArgumentType{
Name: "first",
Expected: "array",
Found: args[0].TypeName(),
}
}
s2, ok := objects.ToString(args[1])
if !ok {
return nil, objects.ErrInvalidArgumentType{
Name: "second",
Expected: "string(compatible)",
Found: args[1].TypeName(),
}
}
// make sure output length does not exceed the limit
if slen+len(s2)*(len(ss1)-1) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
return &objects.String{Value: strings.Join(ss1, s2)}, nil
}
func textFormatBool(args ...objects.Object) (ret objects.Object, err error) {
if len(args) != 1 {
err = objects.ErrWrongNumArguments
@ -583,3 +686,52 @@ func textParseInt(args ...objects.Object) (ret objects.Object, err error) {
return
}
// Modified implementation of strings.Replace
// to limit the maximum length of output string.
func doTextReplace(s, old, new string, n int) (string, bool) {
if old == new || n == 0 {
return s, true // avoid allocation
}
// Compute number of replacements.
if m := strings.Count(s, old); m == 0 {
return s, true // avoid allocation
} else if n < 0 || m < n {
n = m
}
// Apply replacements to buffer.
t := make([]byte, len(s)+n*(len(new)-len(old)))
w := 0
start := 0
for i := 0; i < n; i++ {
j := start
if len(old) == 0 {
if i > 0 {
_, wid := utf8.DecodeRuneInString(s[start:])
j += wid
}
} else {
j += strings.Index(s[start:], old)
}
ssj := s[start:j]
if w+len(ssj)+len(new) > tengo.MaxStringLen {
return "", false
}
w += copy(t[w:], ssj)
w += copy(t[w:], new)
start = j + len(old)
}
ss := s[start:]
if w+len(ss) > tengo.MaxStringLen {
return "", false
}
w += copy(t[w:], ss)
return string(t[0:w]), true
}

View File

@ -3,6 +3,7 @@ package stdlib
import (
"regexp"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -141,7 +142,12 @@ func makeTextRegexp(re *regexp.Regexp) *objects.ImmutableMap {
return
}
ret = &objects.String{Value: re.ReplaceAllString(s1, s2)}
s, ok := doTextRegexpReplace(re, s1, s2)
if !ok {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
return
},
@ -193,3 +199,30 @@ func makeTextRegexp(re *regexp.Regexp) *objects.ImmutableMap {
},
}
}
// Size-limit checking implementation of regexp.ReplaceAllString.
func doTextRegexpReplace(re *regexp.Regexp, src, repl string) (string, bool) {
idx := 0
out := ""
for _, m := range re.FindAllStringSubmatchIndex(src, -1) {
var exp []byte
exp = re.ExpandString(exp, repl, src, m)
if len(out)+m[0]-idx+len(exp) > tengo.MaxStringLen {
return "", false
}
out += src[idx:m[0]] + string(exp)
idx = m[1]
}
if idx < len(src) {
if len(out)+len(src)-idx > tengo.MaxStringLen {
return "", false
}
out += src[idx:]
}
return string(out), true
}

View File

@ -3,6 +3,7 @@ package stdlib
import (
"time"
"github.com/d5/tengo"
"github.com/d5/tengo/objects"
)
@ -867,7 +868,13 @@ func timesTimeFormat(args ...objects.Object) (ret objects.Object, err error) {
return
}
ret = &objects.String{Value: t1.Format(s2)}
s := t1.Format(s2)
if len(s) > tengo.MaxStringLen {
return nil, objects.ErrStringLimit
}
ret = &objects.String{Value: s}
return
}

11
vendor/github.com/d5/tengo/tengo.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
package tengo
var (
// MaxStringLen is the maximum byte-length for string value.
// Note this limit applies to all compiler/VM instances in the process.
MaxStringLen = 2147483647
// MaxBytesLen is the maximum length for bytes value.
// Note this limit applies to all compiler/VM instances in the process.
MaxBytesLen = 2147483647
)

6
vendor/modules.txt vendored
View File

@ -17,14 +17,16 @@ github.com/Philipp15b/go-steam/rwu
github.com/Philipp15b/go-steam/socialcache
# github.com/bwmarrin/discordgo v0.19.0
github.com/bwmarrin/discordgo
# github.com/d5/tengo v1.9.2
# github.com/d5/tengo v1.20.0
github.com/d5/tengo/script
github.com/d5/tengo/stdlib
github.com/d5/tengo/compiler
github.com/d5/tengo/compiler/parser
github.com/d5/tengo/compiler/source
github.com/d5/tengo/objects
github.com/d5/tengo/runtime
github.com/d5/tengo/stdlib
github.com/d5/tengo
github.com/d5/tengo/stdlib/json
github.com/d5/tengo/compiler/ast
github.com/d5/tengo/compiler/token
github.com/d5/tengo/compiler/scanner