Skip to content

Commit

Permalink
Add ExportWithMap method.
Browse files Browse the repository at this point in the history
This provides support for exporting lower-case method names on
DBus while still using the nifty Export mechanism.

Also fix the panic that results from attempting to call an
unexported method.

Also add several tests for verifying the Export and
ExportWithMap functionality.

This resolves guelfey#20.

Signed-off-by: Kyle Fazzari <[email protected]>
  • Loading branch information
Kyle Fazzari committed Jul 27, 2015
1 parent a5942de commit a3eaefb
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 11 deletions.
4 changes: 2 additions & 2 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Conn struct {
calls map[uint32]*Call
callsLck sync.RWMutex

handlers map[ObjectPath]map[string]interface{}
handlers map[ObjectPath]map[string]exportWithMapping
handlersLck sync.RWMutex

out chan *Message
Expand Down Expand Up @@ -157,7 +157,7 @@ func newConn(tr transport) (*Conn, error) {
conn.transport = tr
conn.calls = make(map[uint32]*Call)
conn.out = make(chan *Message, 10)
conn.handlers = make(map[ObjectPath]map[string]interface{})
conn.handlers = make(map[ObjectPath]map[string]exportWithMapping)
conn.nextSerial = 1
conn.serialUsed = map[uint32]bool{0: true}
conn.busObj = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
Expand Down
70 changes: 61 additions & 9 deletions export.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package dbus

import (
"errors"
"fmt"
"reflect"
"strings"
"unicode"
)

var (
Expand All @@ -22,16 +22,49 @@ var (
}
)

// exportWithMapping represents an exported struct along with a method name
// mapping to allow for exporting lower-case methods, etc.
type exportWithMapping struct {
export interface{}

// Method name mapping; key -> struct method, value -> dbus method.
mapping map[string]string
}

// Sender is a type which can be used in exported methods to receive the message
// sender.
type Sender string

func exportedMethod(v interface{}, name string) reflect.Value {
if v == nil {
func exportedMethod(export exportWithMapping, name string) reflect.Value {
if export.export == nil {
return reflect.Value{}
}
m := reflect.ValueOf(v).MethodByName(name)
if !m.IsValid() {

// If a mapping was included in the export, check the map to see if we
// should be looking for a different method in the export.
if export.mapping != nil {
for key, value := range export.mapping {
if value == name {
name = key
break
}

// Catch the case where a method is aliased but the client is calling
// the original, e.g. the "Foo" method was exported mapped to
// "foo," and dbus client called the original "Foo."
if key == name {
return reflect.Value{}
}
}
}

value := reflect.ValueOf(export.export)
m := value.MethodByName(name)

// Catch the case of attempting to call an unexported method
method, ok := value.Type().MethodByName(name)

if !m.IsValid() || !ok || method.PkgPath != "" {
return reflect.Value{}
}
t := m.Type()
Expand Down Expand Up @@ -62,7 +95,7 @@ func (conn *Conn) handleCall(msg *Message) {
}
return
}
if len(name) == 0 || unicode.IsLower([]rune(name)[0]) {
if len(name) == 0 {
conn.sendError(errmsgUnknownMethod, sender, serial)
}
var m reflect.Value
Expand Down Expand Up @@ -214,24 +247,43 @@ func (conn *Conn) Emit(path ObjectPath, name string, values ...interface{}) erro
//
// Export returns an error if path is not a valid path name.
func (conn *Conn) Export(v interface{}, path ObjectPath, iface string) error {
return conn.ExportWithMap(v, nil, path, iface)
}

// ExportWithMap works exactly like Export but provides the ability to remap
// method names (e.g. export a lower-case method).
//
// The keys in the map are the real method names (exported on the struct), and
// the values are the method names to be exported on DBus.
func (conn *Conn) ExportWithMap(v interface{}, mapping map[string]string, path ObjectPath, iface string) error {
if !path.IsValid() {
return errors.New("dbus: invalid path name")
return fmt.Errorf(`dbus: Invalid path name: "%s"`, path)
}

conn.handlersLck.Lock()
defer conn.handlersLck.Unlock()

// Remove a previous export if the interface is nil
if v == nil {
if _, ok := conn.handlers[path]; ok {
delete(conn.handlers[path], iface)
if len(conn.handlers[path]) == 0 {
delete(conn.handlers, path)
}
}

return nil
}

// If this is the first handler for this path, make a new map to hold all
// handlers for this path.
if _, ok := conn.handlers[path]; !ok {
conn.handlers[path] = make(map[string]interface{})
conn.handlers[path] = make(map[string]exportWithMapping)
}
conn.handlers[path][iface] = v

// Finally, save this handler
conn.handlers[path][iface] = exportWithMapping{export: v, mapping: mapping}

return nil
}

Expand Down
122 changes: 122 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package dbus

import "testing"

type lowerCaseExport struct{}

func (export lowerCaseExport) foo() (string, *Error) {
return "bar", nil
}

// Test typical Export usage.
func TestExport(t *testing.T) {
connection, err := SessionBus()
if err != nil {
t.Fatalf("Unexpected error connecting to session bus: %s", err)
}

name := connection.Names()[0]

connection.Export(server{}, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test")
object := connection.Object(name, "/org/guelfey/DBus/Test")

var response int64
err = object.Call("org.guelfey.DBus.Test.Double", 0, int64(2)).Store(&response)
if err != nil {
t.Errorf("Unexpected error calling Double: %s", err)
}

if response != 4 {
t.Errorf("Response was %d, expected 4", response)
}

// Now remove export
connection.Export(nil, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test")
err = object.Call("org.guelfey.DBus.Test.Double", 0, int64(2)).Store(&response)
if err == nil {
t.Error("Expected an error since the export was removed")
}
}

// Test Export with an invalid path.
func TestExport_invalidPath(t *testing.T) {
connection, err := SessionBus()
if err != nil {
t.Fatalf("Unexpected error connecting to session bus: %s", err)
}

err = connection.Export(nil, "foo", "bar")
if err == nil {
t.Error("Expected an error due to exporting with an invalid path")
}
}

// Test Export with an un-exported method. This should not panic, but rather
// result in an invalid method call.
func TestExport_unexportedMethod(t *testing.T) {
connection, err := SessionBus()
if err != nil {
t.Fatalf("Unexpected error connecting to session bus: %s", err)
}

name := connection.Names()[0]

connection.Export(lowerCaseExport{}, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test")
object := connection.Object(name, "/org/guelfey/DBus/Test")

var response string
call := object.Call("org.guelfey.DBus.Test.foo", 0)
err = call.Store(&response)
if err == nil {
t.Errorf("Expected an error due to calling unexported method")
}
}

// Test typical ExportWithMap usage.
func TestExportWithMap(t *testing.T) {
connection, err := SessionBus()
if err != nil {
t.Fatalf("Unexpected error connecting to session bus: %s", err)
}

name := connection.Names()[0]

mapping := make(map[string]string)
mapping["Double"] = "double" // Export this method as lower-case

connection.ExportWithMap(server{}, mapping, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test")
object := connection.Object(name, "/org/guelfey/DBus/Test")

var response int64
err = object.Call("org.guelfey.DBus.Test.double", 0, int64(2)).Store(&response)
if err != nil {
t.Errorf("Unexpected error calling double: %s", err)
}

if response != 4 {
t.Errorf("Response was %d, expected 4", response)
}
}

// Test that ExportWithMap does not export both method alias and method.
func TestExportWithMap_bypassAlias(t *testing.T) {
connection, err := SessionBus()
if err != nil {
t.Fatalf("Unexpected error connecting to session bus: %s", err)
}

name := connection.Names()[0]

mapping := make(map[string]string)
mapping["Double"] = "double" // Export this method as lower-case

connection.ExportWithMap(server{}, mapping, "/org/guelfey/DBus/Test", "org.guelfey.DBus.Test")
object := connection.Object(name, "/org/guelfey/DBus/Test")

var response int64
// Call upper-case Double (i.e. the real method, not the alias)
err = object.Call("org.guelfey.DBus.Test.Double", 0, int64(2)).Store(&response)
if err == nil {
t.Error("Expected an error due to calling actual method, not alias")
}
}

0 comments on commit a3eaefb

Please sign in to comment.