From 5bace2abf4eb835dc1f4432e8ff6a27700a419f7 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 15 Jul 2019 12:52:53 +0200 Subject: [PATCH] Resolve race conditions, panics, and other errors (#1) This patch provides fixes for: - https://github.com/spf13/viper/issues/730 - https://github.com/spf13/viper/issues/695 - https://github.com/spf13/viper/issues/353 - https://github.com/spf13/viper/issues/174 - https://github.com/spf13/viper/issues/378 - https://github.com/spf13/viper/issues/629 --- .circleci/config.yml | 11 +++++ .travis.yml | 2 +- Makefile | 6 +++ README.md | 16 ++++++-- go.mod | 3 +- go.sum | 4 +- remote/remote.go | 3 +- stub/config.json | 12 ++++++ stub/config.yaml | 4 ++ util.go | 25 ++++++++++++ util_test.go | 20 ++++++++++ viper.go | 95 ++++++++++++++++++++++++++++++++++++++++---- viper_test.go | 15 ++++++- 13 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 Makefile create mode 100644 stub/config.json create mode 100644 stub/config.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..d33d61d90 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,11 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.12 + environment: + - GO111MODULE=on + working_directory: /go/src/github.com/ory/viper + steps: + - checkout + - run: go test -race -v ./... diff --git a/.travis.yml b/.travis.yml index d122f22a1..6043447cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -go_import_path: github.com/spf13/viper +go_import_path: github.com/ory/viper language: go diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..5ae6b1e79 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +SHELL=/bin/bash -o pipefail + +# Formats the code +.PHONY: format +format: + goreturns -w -local github.com/ory $$(listx .) diff --git a/README.md b/README.md index 171f51ccb..ba3f8ac84 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,17 @@ ![viper logo](https://cloud.githubusercontent.com/assets/173412/10886745/998df88a-8151-11e5-9448-4736db51020d.png) +[![CircleCI](https://circleci.com/gh/ory/viper/tree/master.svg?style=shield](https://circleci.com/gh/ory/viper/tree/master) + Go configuration with fangs! +> This is a fork. It resolves several issues that are left unresolved in [the upstream](https://github.com/ory/viper). +> Issues resolved and features added include: +> +> - Fixed race conditions when reloading configs. +> - Added `HasChanged(key string) bool` which returns true (once!) when a value has changed. +> - Make sure that `viper.AllSettings()` always returns `map[string]interface{}` which was not the case and incompatible + with de/encoders like `json`. + Many Go projects are built using Viper including: * [Hugo](http://gohugo.io) @@ -13,11 +23,11 @@ Many Go projects are built using Viper including: * [doctl](https://github.com/digitalocean/doctl) * [Clairctl](https://github.com/jgsqware/clairctl) -[![Build Status](https://travis-ci.org/spf13/viper.svg)](https://travis-ci.org/spf13/viper) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/spf13/viper?status.svg)](https://godoc.org/github.com/spf13/viper) +[![Build Status](https://travis-ci.org/ory/viper.svg)](https://travis-ci.org/ory/viper) [![Join the chat at https://gitter.im/ory/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ory/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/ory/viper?status.svg)](https://godoc.org/github.com/ory/viper) ## Install ```console -go get -u github.com/spf13/viper +go get -u github.com/ory/viper ``` ## What is Viper? @@ -384,7 +394,7 @@ viper.BindFlagValues("my-flags", fSet) To enable remote support in Viper, do a blank import of the `viper/remote` package: -`import _ "github.com/spf13/viper/remote"` +`import _ "github.com/ory/viper/remote"` Viper will read a config string (as JSON, TOML, YAML, HCL or envfile) retrieved from a path in a Key/Value store such as etcd or Consul. These values take precedence over diff --git a/go.mod b/go.mod index 9ee26efb9..3f868f5dc 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/spf13/viper +module github.com/ory/viper require ( github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect @@ -28,6 +28,7 @@ require ( github.com/spf13/jwalterweatherman v1.0.0 github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.2.2 + github.com/subosito/gotenv v1.1.1 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect github.com/ugorji/go v1.1.4 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect diff --git a/go.sum b/go.sum index a718a4dd2..c9237e5cb 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -117,6 +115,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/subosito/gotenv v1.1.1 h1:TWxckSF6WVKWbo2M3tMqCtWa9NFUgqM1SSynxmYONOI= +github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= diff --git a/remote/remote.go b/remote/remote.go index 810d0702e..4bf75cd7c 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -11,8 +11,9 @@ import ( "io" "os" - "github.com/spf13/viper" crypt "github.com/xordataexchange/crypt/config" + + "github.com/ory/viper" ) type remoteConfigProvider struct{} diff --git a/stub/config.json b/stub/config.json new file mode 100644 index 000000000..3ace6f54b --- /dev/null +++ b/stub/config.json @@ -0,0 +1,12 @@ +{ + "foo": { + "bar": [ + { + "baz": 1 + }, + { + "baz": 2 + } + ] + } +} \ No newline at end of file diff --git a/stub/config.yaml b/stub/config.yaml new file mode 100644 index 000000000..b914155b8 --- /dev/null +++ b/stub/config.yaml @@ -0,0 +1,4 @@ +foo: + bar: + - baz: 1 + - baz: 2 \ No newline at end of file diff --git a/util.go b/util.go index 952cad44c..7c85222ac 100644 --- a/util.go +++ b/util.go @@ -219,3 +219,28 @@ func deepSearch(m map[string]interface{}, path []string) map[string]interface{} } return m } + +// toMapStringInterface is a workaround for https://github.com/ory/viper/issues/730 +// and https://github.com/go-yaml/yaml/issues/139 +func toMapStringInterface(in interface{}) interface{} { + switch t := in.(type) { + case map[string]interface{}: + for k, v := range t { + t[k] = toMapStringInterface(v) + } + return t + case map[interface{}]interface{}: + nt := make(map[string]interface{}) + for k, v := range t { + nt[fmt.Sprintf("%s", k)] = toMapStringInterface(v) + } + return nt + case []interface{}: + for k, v := range t { + t[k] = toMapStringInterface(v) + } + return t + default: + return in + } +} diff --git a/util_test.go b/util_test.go index 0af80bb63..78ea929cf 100644 --- a/util_test.go +++ b/util_test.go @@ -13,6 +13,8 @@ package viper import ( "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestCopyAndInsensitiviseMap(t *testing.T) { @@ -52,3 +54,21 @@ func TestCopyAndInsensitiviseMap(t *testing.T) { t.Fatal("Input map changed") } } + +func TestToMapStringInterface(t *testing.T) { + assert.EqualValues( + t, + map[string]interface{}{ + "foo": "bar", + "items": map[string]interface{}{ + "foo": "bar", + }, + }, + toMapStringInterface(map[string]interface{}{ + "foo": "bar", + "items": map[interface{}]interface{}{ + "foo": "bar", + }, + }), + ) +} diff --git a/viper.go b/viper.go index 79c334e9d..df1d49cd9 100644 --- a/viper.go +++ b/viper.go @@ -33,14 +33,14 @@ import ( "sync" "time" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" "github.com/fsnotify/fsnotify" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/printer" "github.com/magiconair/properties" "github.com/mitchellh/mapstructure" - toml "github.com/pelletier/go-toml" + "github.com/pelletier/go-toml" "github.com/spf13/afero" "github.com/spf13/cast" jww "github.com/spf13/jwalterweatherman" @@ -205,6 +205,8 @@ type Viper struct { properties *properties.Properties onConfigChange func(fsnotify.Event) + + lock sync.RWMutex } // New returns an initialized Viper instance. @@ -355,6 +357,8 @@ func (v *Viper) WatchConfig() { // Viper will use this and not check any of the config paths. func SetConfigFile(in string) { v.SetConfigFile(in) } func (v *Viper) SetConfigFile(in string) { + v.lock.Lock() + defer v.lock.Unlock() if in != "" { v.configFile = in } @@ -365,6 +369,8 @@ func (v *Viper) SetConfigFile(in string) { // variables that start with "SPF_". func SetEnvPrefix(in string) { v.SetEnvPrefix(in) } func (v *Viper) SetEnvPrefix(in string) { + v.lock.Lock() + defer v.lock.Unlock() if in != "" { v.envPrefix = in } @@ -383,6 +389,8 @@ func (v *Viper) mergeWithEnvPrefix(in string) string { // For backward compatibility reasons this is false by default. func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) } func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) { + v.lock.Lock() + defer v.lock.Unlock() v.allowEmptyEnv = allowEmptyEnv } @@ -404,13 +412,21 @@ func (v *Viper) getEnv(key string) (string, bool) { } // ConfigFileUsed returns the file used to populate the config registry. -func ConfigFileUsed() string { return v.ConfigFileUsed() } -func (v *Viper) ConfigFileUsed() string { return v.configFile } +func ConfigFileUsed() string { return v.ConfigFileUsed() } +func (v *Viper) ConfigFileUsed() string { + v.lock.Lock() + defer v.lock.Unlock() + + return v.configFile +} // AddConfigPath adds a path for Viper to search for the config file in. // Can be called multiple times to define multiple search paths. func AddConfigPath(in string) { v.AddConfigPath(in) } func (v *Viper) AddConfigPath(in string) { + v.lock.Lock() + defer v.lock.Unlock() + if in != "" { absin := absPathify(in) jww.INFO.Println("adding", absin, "to paths to search") @@ -432,6 +448,9 @@ func AddRemoteProvider(provider, endpoint, path string) error { return v.AddRemoteProvider(provider, endpoint, path) } func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error { + v.lock.Lock() + defer v.lock.Unlock() + if !stringInSlice(provider, SupportedRemoteProviders) { return UnsupportedRemoteProviderError(provider) } @@ -464,6 +483,9 @@ func AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) err } func (v *Viper) AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) error { + v.lock.Lock() + defer v.lock.Unlock() + if !stringInSlice(provider, SupportedRemoteProviders) { return UnsupportedRemoteProviderError(provider) } @@ -652,6 +674,9 @@ func (v *Viper) isPathShadowedInAutoEnv(path []string) string { // "a b c" func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) } func (v *Viper) SetTypeByDefaultValue(enable bool) { + v.lock.Lock() + defer v.lock.Unlock() + v.typeByDefValue = enable } @@ -945,6 +970,8 @@ func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) { // func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) } func (v *Viper) BindFlagValue(key string, flag FlagValue) error { + v.lock.Lock() + defer v.lock.Unlock() if flag == nil { return fmt.Errorf("flag for %q is nil", key) } @@ -958,6 +985,8 @@ func (v *Viper) BindFlagValue(key string, flag FlagValue) error { // EnvPrefix will be used when set when env name is not provided. func BindEnv(input ...string) error { return v.BindEnv(input...) } func (v *Viper) BindEnv(input ...string) error { + v.lock.Lock() + defer v.lock.Unlock() var key, envkey string if len(input) == 0 { return fmt.Errorf("BindEnv missing key to bind to") @@ -982,6 +1011,8 @@ func (v *Viper) BindEnv(input ...string) error { // Viper will check to see if an alias exists first. // Note: this assumes a lower-cased key given. func (v *Viper) find(lcaseKey string) interface{} { + v.lock.RLock() + defer v.lock.RUnlock() var ( val interface{} @@ -1132,6 +1163,8 @@ func (v *Viper) IsSet(key string) bool { // keys set in config, default & flags func AutomaticEnv() { v.AutomaticEnv() } func (v *Viper) AutomaticEnv() { + v.lock.Lock() + defer v.lock.Unlock() v.automaticEnvApplied = true } @@ -1140,6 +1173,8 @@ func (v *Viper) AutomaticEnv() { // not match it. func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) } func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) { + v.lock.Lock() + defer v.lock.Unlock() v.envKeyReplacer = r } @@ -1153,6 +1188,8 @@ func (v *Viper) RegisterAlias(alias string, key string) { func (v *Viper) registerAlias(alias string, key string) { alias = strings.ToLower(alias) if alias != key && alias != v.realKey(key) { + v.lock.Lock() + defer v.lock.Unlock() _, exists := v.aliases[alias] if !exists { @@ -1183,6 +1220,8 @@ func (v *Viper) registerAlias(alias string, key string) { } func (v *Viper) realKey(key string) string { + v.lock.RLock() + defer v.lock.RUnlock() newkey, exists := v.aliases[key] if exists { jww.DEBUG.Println("Alias", key, "to", newkey) @@ -1232,6 +1271,9 @@ func (v *Viper) Set(key string, value interface{}) { lastKey := strings.ToLower(path[len(path)-1]) deepestMap := deepSearch(v.override, path[0:len(path)-1]) + v.lock.Lock() + defer v.lock.Unlock() + // set innermost value deepestMap[lastKey] = value } @@ -1263,6 +1305,8 @@ func (v *Viper) ReadInConfig() error { return err } + v.lock.Lock() + defer v.lock.Unlock() v.config = config return nil } @@ -1292,6 +1336,8 @@ func (v *Viper) MergeInConfig() error { // key does not exist in the file. func ReadConfig(in io.Reader) error { return v.ReadConfig(in) } func (v *Viper) ReadConfig(in io.Reader) error { + v.lock.Lock() + defer v.lock.Unlock() v.config = make(map[string]interface{}) return v.unmarshalReader(in, v.config) } @@ -1310,6 +1356,8 @@ func (v *Viper) MergeConfig(in io.Reader) error { // Note that the map given may be modified. func MergeConfigMap(cfg map[string]interface{}) error { return v.MergeConfigMap(cfg) } func (v *Viper) MergeConfigMap(cfg map[string]interface{}) error { + v.lock.Lock() + defer v.lock.Unlock() if v.config == nil { v.config = make(map[string]interface{}) } @@ -1374,13 +1422,19 @@ func (v *Viper) writeConfig(filename string, force bool) error { return fmt.Errorf("File: %s exists. Use WriteConfig to overwrite.", filename) } } + + var buffer bytes.Buffer + if err := v.marshalWriter(&buffer, configType); err != nil { + return err + } + f, err := v.fs.OpenFile(filename, flags, v.configPermissions) if err != nil { return err } defer f.Close() - if err := v.marshalWriter(f, configType); err != nil { + if _, err := io.Copy(f, &buffer); err != nil { return err } @@ -1456,11 +1510,16 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error { return nil } +type fileWriter interface { + io.Writer + io.StringWriter +} + // Marshal a map into Writer. func marshalWriter(f afero.File, configType string) error { return v.marshalWriter(f, configType) } -func (v *Viper) marshalWriter(f afero.File, configType string) error { +func (v *Viper) marshalWriter(f fileWriter, configType string) error { c := v.AllSettings() switch configType { case "json": @@ -1648,8 +1707,11 @@ func (v *Viper) WatchRemoteConfigOnChannel() error { // Retrieve the first found remote configuration. func (v *Viper) getKeyValueConfig() error { + v.lock.RLock() + defer v.lock.RUnlock() + if RemoteConfig == nil { - return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/spf13/viper/remote'") + return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/ory/viper/remote'") } for _, rp := range v.remoteProviders { @@ -1664,6 +1726,9 @@ func (v *Viper) getKeyValueConfig() error { } func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}, error) { + v.lock.RLock() + defer v.lock.RUnlock() + reader, err := RemoteConfig.Get(provider) if err != nil { return nil, err @@ -1715,6 +1780,9 @@ func (v *Viper) watchRemoteConfig(provider RemoteProvider) (map[string]interface // Nested keys are returned with a v.keyDelim (= ".") separator func AllKeys() []string { return v.AllKeys() } func (v *Viper) AllKeys() []string { + v.lock.RLock() + defer v.lock.RUnlock() + m := map[string]bool{} // add all paths, by order of descending priority to ensure correct shadowing m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "") @@ -1810,12 +1878,15 @@ func (v *Viper) AllSettings() map[string]interface{} { // set innermost value deepestMap[lastKey] = value } - return m + return toMapStringInterface(m).(map[string]interface{}) // This is safe because the input is map[string]interface{} } // SetFs sets the filesystem to use to read configuration. func SetFs(fs afero.Fs) { v.SetFs(fs) } func (v *Viper) SetFs(fs afero.Fs) { + v.lock.Lock() + defer v.lock.Unlock() + v.fs = fs } @@ -1824,6 +1895,8 @@ func (v *Viper) SetFs(fs afero.Fs) { func SetConfigName(in string) { v.SetConfigName(in) } func (v *Viper) SetConfigName(in string) { if in != "" { + v.lock.Lock() + defer v.lock.Unlock() v.configName = in v.configFile = "" } @@ -1834,6 +1907,8 @@ func (v *Viper) SetConfigName(in string) { func SetConfigType(in string) { v.SetConfigType(in) } func (v *Viper) SetConfigType(in string) { if in != "" { + v.lock.Lock() + defer v.lock.Unlock() v.configType = in } } @@ -1841,6 +1916,8 @@ func (v *Viper) SetConfigType(in string) { // SetConfigPermissions sets the permissions for the config file. func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) } func (v *Viper) SetConfigPermissions(perm os.FileMode) { + v.lock.Lock() + defer v.lock.Unlock() v.configPermissions = perm.Perm() } @@ -1905,6 +1982,8 @@ func (v *Viper) findConfigFile() (string, error) { // purposes. func Debug() { v.Debug() } func (v *Viper) Debug() { + v.lock.RLock() + defer v.lock.RUnlock() fmt.Printf("Aliases:\n%#v\n", v.aliases) fmt.Printf("Override:\n%#v\n", v.override) fmt.Printf("PFlags:\n%#v\n", v.pflags) diff --git a/viper_test.go b/viper_test.go index f91791f0e..387bfc567 100644 --- a/viper_test.go +++ b/viper_test.go @@ -245,7 +245,7 @@ func initDirs(t *testing.T) (string, string, func()) { } } -//stubs for PFlag Values +// stubs for PFlag Values type stringValue string func newStringValue(val string, p *string) *stringValue { @@ -763,7 +763,7 @@ func TestBindPFlag(t *testing.T) { assert.Equal(t, testString, Get("testvalue")) flag.Value.Set("testing_mutate") - flag.Changed = true //hack for pflag usage + flag.Changed = true // hack for pflag usage assert.Equal(t, "testing_mutate", Get("testvalue")) @@ -1750,6 +1750,17 @@ func TestWatchFile(t *testing.T) { } +func TestArrayOfObjects(t *testing.T) { + SetConfigType("yml") + require.NoError(t, ReadConfig(bytes.NewBufferString(`foo: + bar: + - baz: 1 + - baz: 2`))) + + SetConfigFile(path.Join(os.TempDir(), fmt.Sprintf("config-%d.json", time.Now().UnixNano()))) + require.NoError(t, WriteConfig()) +} + func BenchmarkGetBool(b *testing.B) { key := "BenchmarkGetBool" v = New()