From a3eaefb36f2b439041eb6c3407eda865451f25d5 Mon Sep 17 00:00:00 2001 From: Kyle Fazzari Date: Thu, 23 Jul 2015 12:45:04 -0400 Subject: [PATCH] Add ExportWithMap method. 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 #20. Signed-off-by: Kyle Fazzari --- conn.go | 4 +- export.go | 70 ++++++++++++++++++++++++---- export_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 export_test.go diff --git a/conn.go b/conn.go index 18027a0..e3f93ec 100644 --- a/conn.go +++ b/conn.go @@ -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 @@ -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") diff --git a/export.go b/export.go index f7c0a5c..4888bc4 100644 --- a/export.go +++ b/export.go @@ -2,9 +2,9 @@ package dbus import ( "errors" + "fmt" "reflect" "strings" - "unicode" ) var ( @@ -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() @@ -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 @@ -214,11 +247,23 @@ 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) @@ -226,12 +271,19 @@ func (conn *Conn) Export(v interface{}, path ObjectPath, iface string) error { 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 } diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..d6227b4 --- /dev/null +++ b/export_test.go @@ -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") + } +}