More actions
This commit is contained in:
parent
cd13abdf50
commit
d340cb4c4f
|
@ -6,48 +6,41 @@ import (
|
|||
"github.com/cretz/bine/util"
|
||||
)
|
||||
|
||||
type ConfEntry struct {
|
||||
Key string
|
||||
Value *string
|
||||
}
|
||||
|
||||
func NewConfEntry(key string, value *string) *ConfEntry {
|
||||
return &ConfEntry{Key: key, Value: value}
|
||||
}
|
||||
|
||||
func (c *Conn) SetConf(entries ...*ConfEntry) error {
|
||||
func (c *Conn) SetConf(entries ...*KeyVal) error {
|
||||
return c.sendSetConf("SETCONF", entries)
|
||||
}
|
||||
|
||||
func (c *Conn) ResetConf(entries ...*ConfEntry) error {
|
||||
func (c *Conn) ResetConf(entries ...*KeyVal) error {
|
||||
return c.sendSetConf("RESETCONF", entries)
|
||||
}
|
||||
|
||||
func (c *Conn) sendSetConf(cmd string, entries []*ConfEntry) error {
|
||||
func (c *Conn) sendSetConf(cmd string, entries []*KeyVal) error {
|
||||
for _, entry := range entries {
|
||||
cmd += " " + entry.Key
|
||||
if entry.Value != nil {
|
||||
cmd += "=" + util.EscapeSimpleQuotedStringIfNeeded(*entry.Value)
|
||||
if entry.ValSet() {
|
||||
cmd += "=" + util.EscapeSimpleQuotedStringIfNeeded(entry.Val)
|
||||
}
|
||||
}
|
||||
return c.sendRequestIgnoreResponse(cmd)
|
||||
}
|
||||
|
||||
func (c *Conn) GetConf(keys ...string) ([]*ConfEntry, error) {
|
||||
func (c *Conn) GetConf(keys ...string) ([]*KeyVal, error) {
|
||||
resp, err := c.SendRequest("GETCONF %v", strings.Join(keys, " "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := resp.DataWithReply()
|
||||
ret := make([]*ConfEntry, 0, len(data))
|
||||
ret := make([]*KeyVal, 0, len(data))
|
||||
for _, data := range data {
|
||||
key, val, ok := util.PartitionString(data, '=')
|
||||
entry := &ConfEntry{Key: key}
|
||||
entry := &KeyVal{Key: key}
|
||||
if ok {
|
||||
if val, err = util.UnescapeSimpleQuotedStringIfNeeded(val); err != nil {
|
||||
if entry.Val, err = util.UnescapeSimpleQuotedStringIfNeeded(val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Value = &val
|
||||
if len(entry.Val) == 0 {
|
||||
entry.ValSetAndEmpty = true
|
||||
}
|
||||
}
|
||||
ret = append(ret, entry)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -10,8 +11,21 @@ import (
|
|||
type EventCode string
|
||||
|
||||
const (
|
||||
EventCodeAddrMap EventCode = "ADDRMAP"
|
||||
EventCodeCirc EventCode = "CIRC"
|
||||
EventCodeAddrMap EventCode = "ADDRMAP"
|
||||
EventCodeBandwidth EventCode = "BW"
|
||||
EventCodeCircuit EventCode = "CIRC"
|
||||
EventCodeDescChanged EventCode = "DESCCHANGED"
|
||||
EventCodeLogDebug EventCode = "DEBUG"
|
||||
EventCodeLogErr EventCode = "ERR"
|
||||
EventCodeLogInfo EventCode = "INFO"
|
||||
EventCodeLogNotice EventCode = "NOTICE"
|
||||
EventCodeLogWarn EventCode = "WARN"
|
||||
EventCodeNewDesc EventCode = "NEWDESC"
|
||||
EventCodeORConn EventCode = "ORCONN"
|
||||
EventCodeStatusClient EventCode = "STATUS_CLIENT"
|
||||
EventCodeStatusGeneral EventCode = "STATUS_GENERAL"
|
||||
EventCodeStatusServer EventCode = "STATUS_SERVER"
|
||||
EventCodeStream EventCode = "STREAM"
|
||||
)
|
||||
|
||||
func (c *Conn) AddEventListener(events []EventCode, ch chan<- Event) error {
|
||||
|
@ -67,6 +81,41 @@ func (c *Conn) sendSetEvents() error {
|
|||
return c.sendRequestIgnoreResponse(cmd)
|
||||
}
|
||||
|
||||
func (c *Conn) relayAsyncEvents(resp *Response) {
|
||||
code, data, _ := util.PartitionString(resp.Reply, ' ')
|
||||
// Only relay if there are chans
|
||||
c.eventListenersLock.RLock()
|
||||
chans := c.eventListeners[EventCode(code)]
|
||||
c.eventListenersLock.RUnlock()
|
||||
if len(chans) == 0 {
|
||||
return
|
||||
}
|
||||
// Parse the event
|
||||
// TODO: more events
|
||||
var event Event
|
||||
switch EventCode(code) {
|
||||
case EventCodeCircuit:
|
||||
event = ParseCircuitEvent(data)
|
||||
}
|
||||
if event != nil {
|
||||
for _, ch := range chans {
|
||||
// Just send, if closed or blocking, that's not our problem
|
||||
ch <- event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// zero on fail
|
||||
func parseISOTime(str string) time.Time {
|
||||
// Essentially time.RFC3339 but without 'T' or TZ info
|
||||
const layout = "2006-01-02 15:04:05"
|
||||
ret, err := time.Parse(layout, str)
|
||||
if err != nil {
|
||||
ret = time.Time{}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// zero on fail
|
||||
func parseISOTime2Frac(str string) time.Time {
|
||||
// Essentially time.RFC3339Nano but without TZ info
|
||||
|
@ -78,7 +127,12 @@ func parseISOTime2Frac(str string) time.Time {
|
|||
return ret
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Code() EventCode
|
||||
}
|
||||
|
||||
type CircuitEvent struct {
|
||||
Raw string
|
||||
CircuitID string
|
||||
Status string
|
||||
Path []string
|
||||
|
@ -91,7 +145,6 @@ type CircuitEvent struct {
|
|||
RemoteReason string
|
||||
SocksUsername string
|
||||
SocksPassword string
|
||||
Raw string
|
||||
}
|
||||
|
||||
func ParseCircuitEvent(raw string) *CircuitEvent {
|
||||
|
@ -102,9 +155,7 @@ func ParseCircuitEvent(raw string) *CircuitEvent {
|
|||
var attr string
|
||||
first := true
|
||||
for ok {
|
||||
if attr, raw, ok = util.PartitionString(raw, ' '); !ok {
|
||||
break
|
||||
}
|
||||
attr, raw, ok = util.PartitionString(raw, ' ')
|
||||
key, val, _ := util.PartitionString(attr, '=')
|
||||
switch key {
|
||||
case "BUILD_FLAGS":
|
||||
|
@ -135,32 +186,198 @@ func ParseCircuitEvent(raw string) *CircuitEvent {
|
|||
return event
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Code() EventCode
|
||||
func (*CircuitEvent) Code() EventCode { return EventCodeCircuit }
|
||||
|
||||
type StreamEvent struct {
|
||||
Raw string
|
||||
StreamID string
|
||||
Status string
|
||||
CircuitID string
|
||||
TargetAddress string
|
||||
TargetPort int
|
||||
Reason string
|
||||
RemoteReason string
|
||||
Source string
|
||||
SourceAddress string
|
||||
SourcePort int
|
||||
Purpose string
|
||||
}
|
||||
|
||||
func (*CircuitEvent) Code() EventCode { return EventCodeCirc }
|
||||
|
||||
func (c *Conn) relayAsyncEvents(resp *Response) {
|
||||
code, data, _ := util.PartitionString(resp.Reply, ' ')
|
||||
// Only relay if there are chans
|
||||
c.eventListenersLock.RLock()
|
||||
chans := c.eventListeners[EventCode(code)]
|
||||
c.eventListenersLock.RUnlock()
|
||||
if len(chans) == 0 {
|
||||
return
|
||||
func ParseStreamEvent(raw string) *StreamEvent {
|
||||
event := &StreamEvent{Raw: raw}
|
||||
event.StreamID, raw, _ = util.PartitionString(raw, ' ')
|
||||
event.Status, raw, _ = util.PartitionString(raw, ' ')
|
||||
event.CircuitID, raw, _ = util.PartitionString(raw, ' ')
|
||||
var ok bool
|
||||
event.TargetAddress, raw, ok = util.PartitionString(raw, ' ')
|
||||
if target, port, hasPort := util.PartitionStringFromEnd(event.TargetAddress, ':'); hasPort {
|
||||
event.TargetAddress = target
|
||||
event.TargetPort, _ = strconv.Atoi(port)
|
||||
}
|
||||
// Parse the event
|
||||
// TODO: more events
|
||||
var event Event
|
||||
switch EventCode(code) {
|
||||
case EventCodeCirc:
|
||||
event = ParseCircuitEvent(data)
|
||||
}
|
||||
if event != nil {
|
||||
for _, ch := range chans {
|
||||
// Just send, if closed or blocking, that's not our problem
|
||||
ch <- event
|
||||
var attr string
|
||||
for ok {
|
||||
attr, raw, ok = util.PartitionString(raw, ' ')
|
||||
key, val, _ := util.PartitionString(attr, '=')
|
||||
switch key {
|
||||
case "REASON":
|
||||
event.Reason = val
|
||||
case "REMOTE_REASON":
|
||||
event.RemoteReason = val
|
||||
case "SOURCE":
|
||||
event.Source = val
|
||||
case "SOURCE_ADDR":
|
||||
event.SourceAddress = val
|
||||
if source, port, hasPort := util.PartitionStringFromEnd(event.SourceAddress, ':'); hasPort {
|
||||
event.SourceAddress = source
|
||||
event.SourcePort, _ = strconv.Atoi(port)
|
||||
}
|
||||
case "PURPOSE":
|
||||
event.Purpose = val
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (*StreamEvent) Code() EventCode { return EventCodeStream }
|
||||
|
||||
type ORConnEvent struct {
|
||||
Raw string
|
||||
Target string
|
||||
Status string
|
||||
Reason string
|
||||
NumCircuits int
|
||||
ConnID string
|
||||
}
|
||||
|
||||
func ParseORConnEvent(raw string) *ORConnEvent {
|
||||
event := &ORConnEvent{Raw: raw}
|
||||
event.Target, raw, _ = util.PartitionString(raw, ' ')
|
||||
var ok bool
|
||||
event.Status, raw, ok = util.PartitionString(raw, ' ')
|
||||
var attr string
|
||||
for ok {
|
||||
attr, raw, ok = util.PartitionString(raw, ' ')
|
||||
key, val, _ := util.PartitionString(attr, '=')
|
||||
switch key {
|
||||
case "REASON":
|
||||
event.Reason = val
|
||||
case "NCIRCS":
|
||||
event.NumCircuits, _ = strconv.Atoi(val)
|
||||
case "ID":
|
||||
event.ConnID = val
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (*ORConnEvent) Code() EventCode { return EventCodeORConn }
|
||||
|
||||
type BandwidthEvent struct {
|
||||
Raw string
|
||||
BytesRead int64
|
||||
BytesWritten int64
|
||||
}
|
||||
|
||||
func ParseBandwidthEvent(raw string) *BandwidthEvent {
|
||||
event := &BandwidthEvent{Raw: raw}
|
||||
var temp string
|
||||
temp, raw, _ = util.PartitionString(raw, ' ')
|
||||
event.BytesRead, _ = strconv.ParseInt(temp, 10, 64)
|
||||
temp, raw, _ = util.PartitionString(raw, ' ')
|
||||
event.BytesWritten, _ = strconv.ParseInt(temp, 10, 64)
|
||||
return event
|
||||
}
|
||||
|
||||
func (*BandwidthEvent) Code() EventCode { return EventCodeBandwidth }
|
||||
|
||||
type LogEvent struct {
|
||||
Severity EventCode
|
||||
Raw string
|
||||
}
|
||||
|
||||
func ParseLogEvent(severity EventCode, raw string) *LogEvent {
|
||||
return &LogEvent{Severity: severity, Raw: raw}
|
||||
}
|
||||
|
||||
func (l *LogEvent) Code() EventCode { return l.Severity }
|
||||
|
||||
type NewDescEvent struct {
|
||||
Raw string
|
||||
Descs []string
|
||||
}
|
||||
|
||||
func ParseNewDescEvent(raw string) *NewDescEvent {
|
||||
return &NewDescEvent{Raw: raw, Descs: strings.Split(raw, " ")}
|
||||
}
|
||||
|
||||
func (*NewDescEvent) Code() EventCode { return EventCodeNewDesc }
|
||||
|
||||
type AddrMapEvent struct {
|
||||
Raw string
|
||||
Address string
|
||||
NewAddress string
|
||||
ErrorCode string
|
||||
// Zero if no expire
|
||||
Expires time.Time
|
||||
// Sans double quotes
|
||||
Cached string
|
||||
}
|
||||
|
||||
func ParseAddrMapEvent(raw string) *AddrMapEvent {
|
||||
event := &AddrMapEvent{Raw: raw}
|
||||
event.Address, raw, _ = util.PartitionString(raw, ' ')
|
||||
event.NewAddress, raw, _ = util.PartitionString(raw, ' ')
|
||||
var ok bool
|
||||
// Skip local expiration, use UTC one later
|
||||
_, raw, ok = util.PartitionString(raw, ' ')
|
||||
var attr string
|
||||
for ok {
|
||||
attr, raw, ok = util.PartitionString(raw, ' ')
|
||||
key, val, _ := util.PartitionString(attr, '=')
|
||||
switch key {
|
||||
case "error":
|
||||
event.ErrorCode = val
|
||||
case "EXPIRES":
|
||||
event.Expires = parseISOTime(val)
|
||||
case "CACHED":
|
||||
event.Cached, _ = util.UnescapeSimpleQuotedStringIfNeeded(val)
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (*AddrMapEvent) Code() EventCode { return EventCodeAddrMap }
|
||||
|
||||
type DescChangedEvent struct {
|
||||
Raw string
|
||||
}
|
||||
|
||||
func ParseDescChangedEvent(raw string) *DescChangedEvent {
|
||||
return &DescChangedEvent{Raw: raw}
|
||||
}
|
||||
|
||||
func (*DescChangedEvent) Code() EventCode { return EventCodeDescChanged }
|
||||
|
||||
type StatusEvent struct {
|
||||
Raw string
|
||||
Type EventCode
|
||||
Severity string
|
||||
Action string
|
||||
Arguments map[string]string
|
||||
}
|
||||
|
||||
func ParseStatusEvent(typ EventCode, raw string) *StatusEvent {
|
||||
event := &StatusEvent{Raw: raw, Type: typ, Arguments: map[string]string{}}
|
||||
event.Severity, raw, _ = util.PartitionString(raw, ' ')
|
||||
var ok bool
|
||||
event.Action, raw, ok = util.PartitionString(raw, ' ')
|
||||
var attr string
|
||||
for ok {
|
||||
attr, raw, ok = util.PartitionString(raw, ' ')
|
||||
key, val, _ := util.PartitionString(attr, '=')
|
||||
event.Arguments[key], _ = util.UnescapeSimpleQuotedStringIfNeeded(val)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (s *StatusEvent) Code() EventCode { return s.Type }
|
||||
|
|
|
@ -14,48 +14,34 @@ func (c *Conn) Quit() error {
|
|||
return c.sendRequestIgnoreResponse("QUIT")
|
||||
}
|
||||
|
||||
type MappedAddress struct {
|
||||
Old string
|
||||
New string
|
||||
}
|
||||
|
||||
func NewMappedAddress(old string, new string) *MappedAddress {
|
||||
return &MappedAddress{Old: old, New: new}
|
||||
}
|
||||
|
||||
func (c *Conn) MapAddresses(addresses []*MappedAddress) ([]*MappedAddress, error) {
|
||||
func (c *Conn) MapAddresses(addresses ...*KeyVal) ([]*KeyVal, error) {
|
||||
cmd := "MAPADDRESS"
|
||||
for _, address := range addresses {
|
||||
cmd += " " + address.New + "=" + address.Old
|
||||
cmd += " " + address.Key + "=" + address.Val
|
||||
}
|
||||
resp, err := c.SendRequest(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := resp.DataWithReply()
|
||||
ret := make([]*MappedAddress, 0, len(data))
|
||||
ret := make([]*KeyVal, 0, len(data))
|
||||
for _, address := range data {
|
||||
mappedAddress := &MappedAddress{}
|
||||
mappedAddress.Old, mappedAddress.New, _ = util.PartitionString(address, '=')
|
||||
mappedAddress := &KeyVal{}
|
||||
mappedAddress.Key, mappedAddress.Val, _ = util.PartitionString(address, '=')
|
||||
ret = append(ret, mappedAddress)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type InfoValue struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (c *Conn) GetInfo(keys ...string) ([]*InfoValue, error) {
|
||||
func (c *Conn) GetInfo(keys ...string) ([]*KeyVal, error) {
|
||||
resp, err := c.SendRequest("GETINFO %v", strings.Join(keys, " "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]*InfoValue, 0, len(resp.Data))
|
||||
ret := make([]*KeyVal, 0, len(resp.Data))
|
||||
for _, val := range resp.Data {
|
||||
infoVal := &InfoValue{}
|
||||
infoVal.Key, infoVal.Value, _ = util.PartitionString(val, '=')
|
||||
infoVal := &KeyVal{}
|
||||
infoVal.Key, infoVal.Val, _ = util.PartitionString(val, '=')
|
||||
ret = append(ret, infoVal)
|
||||
}
|
||||
return ret, nil
|
||||
|
|
|
@ -30,6 +30,6 @@ func TestAuthenticateHashedPassword(t *testing.T) {
|
|||
// Verify auth methods before auth
|
||||
info, err := conn.ProtocolInfo()
|
||||
ctx.Require.NoError(err)
|
||||
ctx.Require.ElementsMatch([]string{"HASHESPASSWORD"}, info.AuthMethods)
|
||||
ctx.Require.ElementsMatch([]string{"HASHEDPASSWORD"}, info.AuthMethods)
|
||||
ctx.Require.NoError(conn.Authenticate("testpass"))
|
||||
}
|
||||
|
|
|
@ -15,19 +15,18 @@ func TestGetSetAndResetConf(t *testing.T) {
|
|||
entries, err := conn.GetConf("LogMessageDomains", "ProtocolWarnings")
|
||||
ctx.Require.NoError(err)
|
||||
ctx.Require.Len(entries, 2)
|
||||
ctx.Require.Contains(entries, &control.ConfEntry{Key: "LogMessageDomains", Value: &val})
|
||||
ctx.Require.Contains(entries, &control.ConfEntry{Key: "ProtocolWarnings", Value: &val})
|
||||
ctx.Require.Contains(entries, control.NewKeyVal("LogMessageDomains", val))
|
||||
ctx.Require.Contains(entries, control.NewKeyVal("ProtocolWarnings", val))
|
||||
}
|
||||
assertConfVals("0")
|
||||
// Change em both to 1
|
||||
one := "1"
|
||||
err := conn.SetConf(&control.ConfEntry{Key: "LogMessageDomains", Value: &one},
|
||||
&control.ConfEntry{Key: "ProtocolWarnings", Value: &one})
|
||||
err := conn.SetConf(control.KeyVals("LogMessageDomains", "1", "ProtocolWarnings", "1")...)
|
||||
ctx.Require.NoError(err)
|
||||
// Check again
|
||||
assertConfVals(one)
|
||||
// Reset em both
|
||||
err = conn.ResetConf(&control.ConfEntry{Key: "LogMessageDomains"}, &control.ConfEntry{Key: "ProtocolWarnings"})
|
||||
err = conn.ResetConf(control.KeyVals("LogMessageDomains", "", "ProtocolWarnings", "")...)
|
||||
ctx.Require.NoError(err)
|
||||
// Make sure both back to zero
|
||||
assertConfVals("0")
|
||||
|
@ -41,7 +40,7 @@ func TestLoadConf(t *testing.T) {
|
|||
ctx.Require.NoError(err)
|
||||
ctx.Require.Len(vals, 1)
|
||||
ctx.Require.Equal("config-text", vals[0].Key)
|
||||
confText := vals[0].Value
|
||||
confText := vals[0].Val
|
||||
// Append new conf val and load
|
||||
ctx.Require.NotContains(confText, "LogMessageDomains")
|
||||
confText += "\r\nLogMessageDomains 1"
|
||||
|
@ -51,7 +50,7 @@ func TestLoadConf(t *testing.T) {
|
|||
ctx.Require.NoError(err)
|
||||
ctx.Require.Len(vals, 1)
|
||||
ctx.Require.Equal("config-text", vals[0].Key)
|
||||
ctx.Require.Contains(vals[0].Value, "LogMessageDomains 1")
|
||||
ctx.Require.Contains(vals[0].Val, "LogMessageDomains 1")
|
||||
}
|
||||
|
||||
func TestSaveConf(t *testing.T) {
|
||||
|
@ -62,7 +61,7 @@ func TestSaveConf(t *testing.T) {
|
|||
ctx.Require.NoError(err)
|
||||
ctx.Require.Len(vals, 1)
|
||||
ctx.Require.Equal("config-file", vals[0].Key)
|
||||
confFile := vals[0].Value
|
||||
confFile := vals[0].Val
|
||||
// Save it
|
||||
ctx.Require.NoError(conn.SaveConf(false))
|
||||
// Read and make sure, say, the DataDirectory is accurate
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package controltest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cretz/bine/control"
|
||||
)
|
||||
|
||||
func TestGetHiddenServiceDescriptorAsync(t *testing.T) {
|
||||
ctx, conn := NewTestContextAuthenticated(t)
|
||||
defer ctx.CloseConnected(conn)
|
||||
// Enable the network
|
||||
ctx.Require.NoError(conn.SetConf(control.NewKeyVal("DisableNetwork", "0")))
|
||||
ctx.Require.NoError(conn.GetHiddenServiceDescriptorAsync("facebookcorewwwi", ""))
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package controltest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSignal(t *testing.T) {
|
||||
ctx, conn := NewTestContextAuthenticated(t)
|
||||
defer ctx.CloseConnected(conn)
|
||||
ctx.Require.NoError(conn.Signal("HEARTBEAT"))
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package control
|
||||
|
||||
type KeyVal struct {
|
||||
Key string
|
||||
Val string
|
||||
// If true and the associated command supports nil vals, then an empty string for val is NOT considered nil like it
|
||||
// otherwise would. This is ignored for commands that don't support nil vals.
|
||||
ValSetAndEmpty bool
|
||||
}
|
||||
|
||||
func NewKeyVal(key string, val string) *KeyVal {
|
||||
return &KeyVal{Key: key, Val: val}
|
||||
}
|
||||
|
||||
func KeyVals(keysAndVals ...string) []*KeyVal {
|
||||
if len(keysAndVals)%2 != 0 {
|
||||
panic("Expected multiple of 2")
|
||||
}
|
||||
ret := make([]*KeyVal, len(keysAndVals)/2)
|
||||
for i := 0; i < len(ret); i++ {
|
||||
ret[i] = NewKeyVal(keysAndVals[i*2], keysAndVals[i*2+1])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (k *KeyVal) ValSet() bool {
|
||||
return len(k.Val) > 0 || k.ValSetAndEmpty
|
||||
}
|
|
@ -13,6 +13,14 @@ func PartitionString(str string, ch byte) (string, string, bool) {
|
|||
return str[:index], str[index+1:], true
|
||||
}
|
||||
|
||||
func PartitionStringFromEnd(str string, ch byte) (string, string, bool) {
|
||||
index := strings.LastIndexByte(str, ch)
|
||||
if index == -1 {
|
||||
return str, "", false
|
||||
}
|
||||
return str[:index], str[index+1:], true
|
||||
}
|
||||
|
||||
func EscapeSimpleQuotedStringIfNeeded(str string) string {
|
||||
if strings.ContainsAny(str, " \\\"\r\n") {
|
||||
return EscapeSimpleQuotedString(str)
|
||||
|
|
Loading…
Reference in New Issue