shiny/screen: sanitize NewWindowOptions.Title.

Its bytes are passed to C libraries, so we sanitize as a precaution.

Change-Id: I6ecdd5388be40c4067815ba0484112bea6c55270
Reviewed-on: https://go-review.googlesource.com/37414
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
diff --git a/shiny/driver/gldriver/cocoa.go b/shiny/driver/gldriver/cocoa.go
index c41ec3c..cc01582 100644
--- a/shiny/driver/gldriver/cocoa.go
+++ b/shiny/driver/gldriver/cocoa.go
@@ -65,14 +65,10 @@
 func newWindow(opts *screen.NewWindowOptions) (uintptr, error) {
 	width, height := optsSize(opts)
 
-	var title string
-	if opts != nil {
-		title = opts.Title
-	}
-	titlePtr := C.CString(title)
-	defer C.free(unsafe.Pointer(titlePtr))
+	title := C.CString(opts.GetTitle())
+	defer C.free(unsafe.Pointer(title))
 
-	return uintptr(C.doNewWindow(C.int(width), C.int(height), titlePtr)), nil
+	return uintptr(C.doNewWindow(C.int(width), C.int(height), title)), nil
 }
 
 func initWindow(w *windowImpl) {
diff --git a/shiny/driver/gldriver/x11.c b/shiny/driver/gldriver/x11.c
index 11d7084..dcb583d 100644
--- a/shiny/driver/gldriver/x11.c
+++ b/shiny/driver/gldriver/x11.c
@@ -231,7 +231,7 @@
 }
 
 uintptr_t
-doNewWindow(int width, int height, char* title) {
+doNewWindow(int width, int height, char* title, int title_len) {
 	XSetWindowAttributes attr;
 	attr.colormap = x_colormap;
 	attr.event_mask =
@@ -260,7 +260,7 @@
 	XSetWMProtocols(x_dpy, win, atoms, 2);
 
 	XSetStandardProperties(x_dpy, win, "", "App", None, (char **)NULL, 0, &sizehints);
-	XChangeProperty(x_dpy, win, wm_name, utf8_string, 8, PropModeReplace, title, strlen(title));
+	XChangeProperty(x_dpy, win, wm_name, utf8_string, 8, PropModeReplace, title, title_len);
 
 	return win;
 }
diff --git a/shiny/driver/gldriver/x11.go b/shiny/driver/gldriver/x11.go
index 0985dd7..0f0e376 100644
--- a/shiny/driver/gldriver/x11.go
+++ b/shiny/driver/gldriver/x11.go
@@ -53,17 +53,13 @@
 func newWindow(opts *screen.NewWindowOptions) (uintptr, error) {
 	width, height := optsSize(opts)
 
-	var title string
-	if opts != nil {
-		title = opts.Title
-	}
-	titlePtr := C.CString(title)
-	defer C.free(unsafe.Pointer(titlePtr))
+	title := C.CString(opts.GetTitle())
+	defer C.free(unsafe.Pointer(title))
 
 	retc := make(chan uintptr)
 	uic <- uiClosure{
 		f: func() uintptr {
-			return uintptr(C.doNewWindow(C.int(width), C.int(height), titlePtr))
+			return uintptr(C.doNewWindow(C.int(width), C.int(height), title, C.int(len(title))))
 		},
 		retc: retc,
 	}
diff --git a/shiny/driver/internal/win32/win32.go b/shiny/driver/internal/win32/win32.go
index 151d198..402b575 100644
--- a/shiny/driver/internal/win32/win32.go
+++ b/shiny/driver/internal/win32/win32.go
@@ -66,11 +66,7 @@
 	if err != nil {
 		return 0, err
 	}
-	var title string
-	if opts != nil {
-		title = opts.Title
-	}
-	windowTitle, err := syscall.UTF16PtrFromString(title)
+	title, err := syscall.UTF16PtrFromString(opts.GetTitle())
 	if err != nil {
 		return 0, err
 	}
@@ -84,7 +80,7 @@
 		}
 	}
 	hwnd, err := _CreateWindowEx(0,
-		wcname, windowTitle,
+		wcname, title,
 		_WS_OVERLAPPEDWINDOW,
 		_CW_USEDEFAULT, _CW_USEDEFAULT,
 		int32(w), int32(h),
diff --git a/shiny/driver/x11driver/screen.go b/shiny/driver/x11driver/screen.go
index 2f9cc71..e3f01af 100644
--- a/shiny/driver/x11driver/screen.go
+++ b/shiny/driver/x11driver/screen.go
@@ -423,10 +423,7 @@
 	)
 	s.setProperty(xw, s.atomWMProtocols, s.atomWMDeleteWindow, s.atomWMTakeFocus)
 
-	var title []byte
-	if opts != nil {
-		title = []byte(opts.Title)
-	}
+	title := []byte(opts.GetTitle())
 	xproto.ChangeProperty(s.xc, xproto.PropModeReplace, xw, xproto.AtomWmName, s.atomUTF8String, 8, uint32(len(title)), title)
 
 	xproto.CreateGC(s.xc, xg, xproto.Drawable(xw), 0, nil)
diff --git a/shiny/screen/screen.go b/shiny/screen/screen.go
index d3c7d7b..5d89fe8 100644
--- a/shiny/screen/screen.go
+++ b/shiny/screen/screen.go
@@ -58,6 +58,7 @@
 	"image"
 	"image/color"
 	"image/draw"
+	"unicode/utf8"
 
 	"golang.org/x/image/math/f64"
 )
@@ -238,6 +239,33 @@
 	// TODO: fullscreen, icon, cursorHidden?
 }
 
+// GetTitle returns a sanitized form of o.Title. In particular, its length will
+// not exceed 4096, and it may be further truncated so that it is valid UTF-8
+// and will not contain the NUL byte.
+//
+// o may be nil, in which case "" is returned.
+func (o *NewWindowOptions) GetTitle() string {
+	if o == nil {
+		return ""
+	}
+	return sanitizeUTF8(o.Title, 4096)
+}
+
+func sanitizeUTF8(s string, n int) string {
+	if n < len(s) {
+		s = s[:n]
+	}
+	i := 0
+	for i < len(s) {
+		r, n := utf8.DecodeRuneInString(s[i:])
+		if r == 0 || (r == utf8.RuneError && n == 1) {
+			break
+		}
+		i += n
+	}
+	return s[:i]
+}
+
 // Uploader is something you can upload a Buffer to.
 type Uploader interface {
 	// Upload uploads the sub-Buffer defined by src and sr to the destination
diff --git a/shiny/screen/screen_test.go b/shiny/screen/screen_test.go
new file mode 100644
index 0000000..bc95011
--- /dev/null
+++ b/shiny/screen/screen_test.go
@@ -0,0 +1,53 @@
+// Copyright 2017 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 screen
+
+import (
+	"testing"
+)
+
+func TestSanitizeUTF8(t *testing.T) {
+	const n = 8
+
+	testCases := []struct {
+		s, want string
+	}{
+		{"", ""},
+		{"a", "a"},
+		{"a\x00", "a"},
+		{"a\x80", "a"},
+		{"\x00a", ""},
+		{"\x80a", ""},
+		{"abc", "abc"},
+		{"foo b\x00r qux", "foo b"},
+		{"foo b\x80r qux", "foo b"},
+		{"foo b\xffr qux", "foo b"},
+
+		// "\xc3\xa0" is U+00E0 LATIN SMALL LETTER A WITH GRAVE.
+		{"\xc3\xa0pqrs", "\u00e0pqrs"},
+		{"a\xc3\xa0pqrs", "a\u00e0pqrs"},
+		{"ab\xc3\xa0pqrs", "ab\u00e0pqrs"},
+		{"abc\xc3\xa0pqrs", "abc\u00e0pqr"},
+		{"abcd\xc3\xa0pqrs", "abcd\u00e0pq"},
+		{"abcde\xc3\xa0pqrs", "abcde\u00e0p"},
+		{"abcdef\xc3\xa0pqrs", "abcdef\u00e0"},
+		{"abcdefg\xc3\xa0pqrs", "abcdefg"},
+		{"abcdefgh\xc3\xa0pqrs", "abcdefgh"},
+		{"abcdefghi\xc3\xa0pqrs", "abcdefgh"},
+		{"abcdefghij\xc3\xa0pqrs", "abcdefgh"},
+
+		// "世" is "\xe4\xb8\x96".
+		// "界" is "\xe7\x95\x8c".
+		{"H 世界", "H 世界"},
+		{"Hi 世界", "Hi 世"},
+		{"Hello 世界", "Hello "},
+	}
+
+	for _, tc := range testCases {
+		if got := sanitizeUTF8(tc.s, n); got != tc.want {
+			t.Errorf("sanitizeUTF8(%q): got %q, want %q", tc.s, got, tc.want)
+		}
+	}
+}