diff --git a/Makefile b/Makefile index 8762b1b1..6fa589b7 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,25 @@ .PHONY: all -all: go-build +all: build ifneq (on,$(GO111MODULE)) export GO111MODULE := on endif -#.PHONY: go-dep -go-dep: +#.PHONY: dep +dep: go mod download && go mod verify -.PHONY: go-build -go-build: +.PHONY: build +build: git config core.hooksPath hooks go build . # # Tests-related tasks # -.PHONY: go-unittest -go-unittest: go-build +.PHONY: unit-test +unit-test: build go test -json ./... -run ^Test -.PHONY: go-coverage -go-coverage: go-build +.PHONY: coverage +coverage: build go test -json -covermode=atomic -coverpkg=./... -coverprofile goisilon_coverprofile.out ./... -run ^Test - diff --git a/acls_test.go b/acls_test.go index 626a340a..51f6b41d 100755 --- a/acls_test.go +++ b/acls_test.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2022 Dell Inc, or its subsidiaries. +Copyright (c) 2022-2025 Dell Inc, or its subsidiaries. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,105 +16,38 @@ limitations under the License. package goisilon import ( - "fmt" "testing" + "github.com/dell/goisilon/mocks" "github.com/stretchr/testify/assert" - - api "github.com/dell/goisilon/api/v2" ) func TestGetVolumeACL(t *testing.T) { - volumeName := "test_get_volume_acl" - - // make sure the volume exists - client.CreateVolume(defaultCtx, volumeName) - volume, err := client.GetVolume(defaultCtx, volumeName, volumeName) - assertNoError(t, err) - assertNotNil(t, volume) - - defer client.DeleteVolume(defaultCtx, volume.Name) - - username := client.API.User() - user, err := client.GetUserByNameOrUID(defaultCtx, &username, nil) - assertNoError(t, err) - assertNotNil(t, user) - - acl, err := client.GetVolumeACL(defaultCtx, volume.Name) - assertNoError(t, err) - assertNotNil(t, acl) - - assertNotNil(t, acl.Owner) - assertNotNil(t, acl.Owner.Name) - assert.Equal(t, user.Name, *acl.Owner.Name) - assertNotNil(t, acl.Owner.Type) - assert.Equal(t, api.PersonaTypeUser, *acl.Owner.Type) - assertNotNil(t, acl.Owner.ID) - assert.Equal(t, user.OnDiskUserIdentity.ID, fmt.Sprintf("UID:%s", acl.Owner.ID.ID)) - assert.Equal(t, api.PersonaIDTypeUID, acl.Owner.ID.Type) + client.API.(*mocks.Client).On("VolumesPath", anyArgs[0:6]...).Return("").Once() + client.API.(*mocks.Client).On("Get", anyArgs[0:6]...).Return(nil).Once() + _, err := client.GetVolumeACL(defaultCtx, "test_get_volume_acl") + assert.Nil(t, err) } func TestSetVolumeOwnerToCurrentUser(t *testing.T) { - volumeName := "test_set_volume_owner" - - // make sure the volume exists - client.CreateVolume(defaultCtx, volumeName) - volume, err := client.GetVolume(defaultCtx, volumeName, volumeName) - assertNoError(t, err) - assertNotNil(t, volume) - - defer client.DeleteVolume(defaultCtx, volume.Name) - - username := client.API.User() - user, err := client.GetUserByNameOrUID(defaultCtx, &username, nil) - assertNoError(t, err) - assertNotNil(t, user) - - acl, err := client.GetVolumeACL(defaultCtx, volume.Name) - assertNoError(t, err) - assertNotNil(t, acl) - - assertNotNil(t, acl.Owner) - assertNotNil(t, acl.Owner.Name) - assert.Equal(t, user.Name, *acl.Owner.Name) - assertNotNil(t, acl.Owner.Type) - assert.Equal(t, api.PersonaTypeUser, *acl.Owner.Type) - assertNotNil(t, acl.Owner.ID) - assert.Equal(t, user.OnDiskUserIdentity.ID, fmt.Sprintf("UID:%s", acl.Owner.ID.ID)) - assert.Equal(t, api.PersonaIDTypeUID, acl.Owner.ID.Type) - - err = client.SetVolumeOwner(defaultCtx, volume.Name, "rexray") - if err != nil { - t.Skipf("Unable to change volume owner: %s - is efs.bam.chown_unrestricted set?", err) - } - assertNoError(t, err) - - acl, err = client.GetVolumeACL(defaultCtx, volume.Name) - assertNoError(t, err) - assertNotNil(t, acl) - - assertNotNil(t, acl.Owner) - assertNotNil(t, acl.Owner.Name) - assert.Equal(t, "rexray", *acl.Owner.Name) - assertNotNil(t, acl.Owner.Type) - assert.Equal(t, api.PersonaTypeUser, *acl.Owner.Type) - assertNotNil(t, acl.Owner.ID) - assert.Equal(t, "2000", acl.Owner.ID.ID) - assert.Equal(t, api.PersonaIDTypeUID, acl.Owner.ID.Type) - - err = client.SetVolumeOwnerToCurrentUser(defaultCtx, volume.Name) - assertNoError(t, err) + client.API.(*mocks.Client).On("VolumesPath", anyArgs[0:6]...).Return("").Once() + client.API.(*mocks.Client).On("Get", anyArgs[0:6]...).Return(nil).Once() + client.API.(*mocks.Client).On("User", anyArgs[0:6]...).Return("").Once() + client.API.(*mocks.Client).On("Put", anyArgs...).Return(nil).Once() + err := client.SetVolumeOwnerToCurrentUser(defaultCtx, "test_set_volume_owner") + assert.Nil(t, err) +} - acl, err = client.GetVolumeACL(defaultCtx, volume.Name) - assertNoError(t, err) - assertNotNil(t, acl) +func TestSetVolumeOwner(t *testing.T) { + client.API.(*mocks.Client).On("VolumesPath", anyArgs[0:6]...).Return("").Once() + client.API.(*mocks.Client).On("Put", anyArgs...).Return(nil).Once() + err := client.SetVolumeOwner(defaultCtx, "test_set_volume_owner", "rexray") + assert.Nil(t, err) +} - assertNotNil(t, acl.Owner) - assertNotNil(t, acl.Owner.Name) - assert.Equal(t, client.API.User(), *acl.Owner.Name) - assertNotNil(t, acl.Owner.Type) - assert.Equal(t, api.PersonaTypeUser, *acl.Owner.Type) - assertNotNil(t, acl.Owner.ID) - assert.Equal(t, "10", acl.Owner.ID.ID) - assert.Equal(t, api.PersonaIDTypeUID, acl.Owner.ID.Type) +func TestSetVolumeMode(t *testing.T) { + client.API.(*mocks.Client).On("VolumesPath", anyArgs[0:6]...).Return("").Once() + client.API.(*mocks.Client).On("Put", anyArgs...).Return(nil).Once() + err := client.SetVolumeMode(defaultCtx, "test_set_volume_owner", 777) + assert.Nil(t, err) } diff --git a/api/api.go b/api/api.go index b299bdb6..8b271256 100755 --- a/api/api.go +++ b/api/api.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "errors" "fmt" "io" @@ -35,7 +36,6 @@ import ( "github.com/sirupsen/logrus" "github.com/PuerkitoBio/goquery" - "github.com/dell/goisilon/api/json" ) const ( @@ -389,6 +389,10 @@ func (c *client) DoWithHeaders( params OrderedValues, headers map[string]string, body, resp interface{}, ) error { + return doWithHeadersFunc(c, ctx, method, uri, id, params, headers, body, resp) +} + +var doWithHeadersFunc = func(c *client, ctx context.Context, method string, uri string, id string, params OrderedValues, headers map[string]string, body, resp interface{}) error { res, _, err := c.DoAndGetResponseBody( ctx, method, uri, id, params, headers, body) if err != nil { @@ -425,6 +429,16 @@ func (c *client) DoAndGetResponseBody( method, uri, id string, params OrderedValues, headers map[string]string, body interface{}, +) (*http.Response, bool, error) { + return doAndGetResponseBodyFunc(c, ctx, method, uri, id, params, headers, body) +} + +var doAndGetResponseBodyFunc = func( + c *client, + ctx context.Context, + method, uri, id string, + params OrderedValues, headers map[string]string, + body interface{}, ) (*http.Response, bool, error) { var ( err error @@ -650,6 +664,10 @@ func parseJSONHTMLError(r *http.Response) error { // Authenticate make a REST API call [/session/1/session] to PowerScale to authenticate the given credentials. // The response contains the session Cookie, X-CSRF-Token and the client uses it for further communication. func (c *client) authenticate(ctx context.Context, username string, password string, endpoint string) error { + return authenticateFunc(c, ctx, username, password, endpoint) +} + +var authenticateFunc = func(c *client, ctx context.Context, username string, password string, endpoint string) error { headers := make(map[string]string, 1) headers[headerKeyContentType] = headerValContentTypeJSON data := &setupConnection{Services: []string{"platform", "namespace"}, Username: username, Password: password} @@ -722,9 +740,9 @@ func (c *client) executeWithRetryAuthenticate(ctx context.Context, method, uri s return fmt.Errorf("authentication failure due to: %v", err) } return c.DoWithHeaders(ctx, method, uri, id, params, headers, body, resp) - } else { - log.Error(ctx, "Error in response. Method:%s URI:%s Error: %v JSON Error: %+v", method, uri, err, e) } + log.Error(ctx, "Error in response. Method:%s URI:%s Error: %v JSON Error: %+v", method, uri, err, e) + case *HTMLError: if e.StatusCode == 401 { log.Debug(ctx, "Authentication failed. Trying to re-authenticate") @@ -732,9 +750,8 @@ func (c *client) executeWithRetryAuthenticate(ctx context.Context, method, uri s return fmt.Errorf("authentication failure due to: %v", err) } return c.DoWithHeaders(ctx, method, uri, id, params, headers, body, resp) - } else { - log.Error(ctx, "Error in response. Method:%s URI:%s Error: %v HTML Error: %+v", method, uri, err, e) } + log.Error(ctx, "Error in response. Method:%s URI:%s Error: %v HTML Error: %+v", method, uri, err, e) default: log.Error(ctx, "Error is not a type of \"*JSONError or *HTMLError\". Error:", err) } diff --git a/api/api_logging_test.go b/api/api_logging_test.go new file mode 100644 index 00000000..4ce6da2a --- /dev/null +++ b/api/api_logging_test.go @@ -0,0 +1,222 @@ +/* +Copyright (c) 2025 Dell Inc, or its subsidiaries. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsBinOctetBody(t *testing.T) { + header := http.Header{} + header.Add(headerKeyContentType, headerValContentTypeBinaryOctetStream) + assert.True(t, isBinOctetBody(header)) + + header.Set(headerKeyContentType, "application/json") + assert.False(t, isBinOctetBody(header)) +} + +func TestLogRequest(t *testing.T) { + req := httptest.NewRequest("GET", "/test-url", nil) + ctx := context.Background() + var out bytes.Buffer + + t.Run("VerboseLow", func(t *testing.T) { + out.Reset() + logRequest(ctx, &out, req, VerboseLow) + assert.Contains(t, out.String(), "GET /test-url ") + }) + + t.Run("VerboseHigh", func(t *testing.T) { + out.Reset() + logRequest(ctx, &out, req, VerboseHigh) + assert.Contains(t, out.String(), "GET /test-url ") + assert.Contains(t, out.String(), "Host: example.com") + }) +} + +func TestLogResponse(t *testing.T) { + res := httptest.NewRecorder().Result() + ctx := context.Background() + var out bytes.Buffer + + t.Run("VerboseLow", func(t *testing.T) { + out.Reset() + logResponse(ctx, res, VerboseLow) + assert.Contains(t, out.String(), "") + }) + + t.Run("VerboseMedium", func(t *testing.T) { + out.Reset() + logResponse(ctx, res, VerboseMedium) + assert.Contains(t, out.String(), "") + }) + + t.Run("VerboseHigh", func(t *testing.T) { + out.Reset() + logResponse(ctx, res, VerboseHigh) + assert.Contains(t, out.String(), "") + }) +} + +func TestWriteIndented(t *testing.T) { + data := []byte("line1\nline2\nline3") + var out bytes.Buffer + err := WriteIndented(&out, data) + assert.NoError(t, err) + assert.Equal(t, " line1\n line2\n line3", out.String()) +} + +type errorWriter struct { + failAfter int + writes int +} + +func (ew *errorWriter) Write(p []byte) (n int, err error) { + if ew.writes >= ew.failAfter { + return 0, errors.New("forced write error") + } + ew.writes++ + return len(p), nil +} + +func TestWriteIndentedN(t *testing.T) { + data := []byte("line1\nline2\nline3") + + // Original test case to ensure it still passes + t.Run("normal case", func(t *testing.T) { + var out bytes.Buffer + err := WriteIndentedN(&out, data, 2) + assert.NoError(t, err) + assert.Equal(t, " line1\n line2\n line3", out.String()) + }) + + // Test case to cover error scenarios + t.Run("error after writing space", func(t *testing.T) { + ew := &errorWriter{failAfter: 1} + err := WriteIndentedN(ew, data, 2) + assert.Error(t, err) + assert.Equal(t, "forced write error", err.Error()) + }) + + t.Run("error after writing line content", func(t *testing.T) { + ew := &errorWriter{failAfter: 3} // 2 spaces + 1 line content = 3 writes + err := WriteIndentedN(ew, data, 2) + assert.Error(t, err) + assert.Equal(t, "forced write error", err.Error()) + }) + + t.Run("error after writing newline", func(t *testing.T) { + ew := &errorWriter{failAfter: 4} // 2 spaces + 1 line content + 1 newline = 4 writes + err := WriteIndentedN(ew, data, 2) + assert.Error(t, err) + assert.Equal(t, "forced write error", err.Error()) + }) +} + +func TestEncryptPassword(t *testing.T) { + reqData := "GET / HTTP/1.1\nAuthorization: Basic " + base64.StdEncoding.EncodeToString([]byte("user:password")) + "\n" + + result := encryptPassword([]byte(reqData)) + expected := "GET / HTTP/1.1\nAuthorization: user:******\n" + assert.Equal(t, expected, string(result)) + + cases := []struct { + name string + input string + expected string + }{ + { + name: "password", + input: `{"password":"my-secret-password"}`, + expected: `{"password":"****"}` + "\n", + }, + { + name: "session id", + input: "Cookie: isisessid=my-session-id", + expected: "Cookie: isisessid=****-session-id\n", + }, + { + name: "CSRF Token", + input: "X-Csrf-Token: my-csrf-token", + expected: "X-Csrf-Token:****-csrf-token\n", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := encryptPassword([]byte(c.input)) + assert.Contains(t, c.expected, string(result)) + }) + } +} + +func TestFetchValueIndexForKey(t *testing.T) { + cases := []struct { + name string + line string + key string + separator string + expStart int + expEnd int + expMatchLen int + }{ + { + name: "no separator", + line: `"password":"my-secret-password"`, + key: `"password":"`, + separator: `"`, + expStart: 0, + expEnd: 18, + expMatchLen: 12, + }, + { + name: "with separator", + line: `Cookie: isisessid=my-session-id; path=/`, + key: `isisessid=`, + separator: `;`, + expStart: 8, + expEnd: 13, + expMatchLen: 10, + }, + { + name: "full key", + line: `X-Csrf-Token: my-csrf-token`, + key: `X-Csrf-Token:`, + separator: ` `, + expStart: 0, + expEnd: 0, + expMatchLen: 13, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + start, end, matchLen := FetchValueIndexForKey(c.line, c.key, c.separator) + assert.Equal(t, c.expStart, start) + assert.Equal(t, c.expEnd, end) + assert.Equal(t, c.expMatchLen, matchLen) + }) + } +} diff --git a/api/api_ordered_values_test.go b/api/api_ordered_values_test.go index 9ba1f0e6..34446472 100644 --- a/api/api_ordered_values_test.go +++ b/api/api_ordered_values_test.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2022 Dell Inc, or its subsidiaries. +Copyright (c) 2022-2025 Dell Inc, or its subsidiaries. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -245,3 +245,16 @@ func TestStructToOrderedValues(t *testing.T) { t.Errorf("StructToOrderedValue(%v) = %v; want %v", params, actual, expected) } } + +func TestStringSet(t *testing.T) { + var v OrderedValues + v.Set([]byte("query"), nil) + v.StringSet("", "") + assert.Equal(t, "query", v.Encode()) + + v.StringSet("key", "") + assert.Equal(t, "query&key", v.Encode()) + + v.StringSet("key", "value") + assert.Equal(t, "query&key=value", v.Encode()) +} diff --git a/api/api_test.go b/api/api_test.go index 6216f396..418bac26 100755 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2022 Dell Inc, or its subsidiaries. +Copyright (c) 2022-2025 Dell Inc, or its subsidiaries. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,11 +16,37 @@ limitations under the License. package api import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) +type ( + EmptyMockBody struct{} + MockBody struct { + ReadFunc func(p []byte) (n int, err error) + CloseFunc func() error + } +) + +func (m *MockBody) Read(p []byte) (n int, err error) { + return m.ReadFunc(p) +} + +func (m *MockBody) Close() error { + return m.CloseFunc() +} + func assertLen(t *testing.T, obj interface{}, expLen int) { if !assert.Len(t, obj, expLen) { t.FailNow() @@ -50,3 +76,633 @@ func assertNotNil(t *testing.T, i interface{}) { t.FailNow() } } + +func newMockHTTPServer(handleReq func(http.ResponseWriter, *http.Request)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleReq(w, r) + })) +} + +func TestNew(t *testing.T) { + getReqHandler := func(serverVersion string) func(http.ResponseWriter, *http.Request) { + if serverVersion != "" { + return func(w http.ResponseWriter, _ *http.Request) { + res := &apiVerResponse{Latest: &serverVersion} + w.WriteHeader(http.StatusOK) + body, err := json.Marshal(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = w.Write(body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + } + return func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + } + } + + serverURL := "server.URL" + + testData := []struct { + testName string + hostname string + username string + password string + groupName string + verboseLogging uint + authType uint8 + opts *ClientOptions + reqHandler func(http.ResponseWriter, *http.Request) + expectedErr string + }{ + { + testName: "Negative: empty call params", + expectedErr: "missing endpoint, username, or password", + }, + { + testName: "Negative: bad hostname", + hostname: "test", + username: "testuser", + password: "testpassword", + authType: 42, // unknown auth type should default to basic + opts: &ClientOptions{ + VolumesPath: "test/volumes", + VolumesPathPermissions: "test/permissions", + IgnoreUnresolvableHosts: true, + Timeout: 10 * time.Second, + Insecure: true, + }, + expectedErr: "unsupported protocol scheme", + }, + { + testName: "Negative: empty server response", + hostname: serverURL, + username: "testuser", + password: "testpassword", + groupName: "testgroup", + verboseLogging: 1, + authType: authTypeSessionBased, + opts: &ClientOptions{ + Insecure: false, + }, + reqHandler: getReqHandler(""), + expectedErr: "OneFS releases older than", + }, + { + testName: "Negative: malformed major version in response", + hostname: serverURL, + username: "testuser", + password: "testpassword", + reqHandler: getReqHandler("a.3"), + expectedErr: "strconv.ParseUint: parsing ", + }, + { + testName: "Negative: malformed minor version in response", + hostname: serverURL, + username: "testuser", + password: "testpassword", + reqHandler: getReqHandler("8.b"), + expectedErr: "strconv.ParseUint: parsing ", + }, + { + testName: "Positive: correct version in response", + hostname: serverURL, + username: "testuser", + password: "testpassword", + reqHandler: getReqHandler("8.3"), + expectedErr: "", + }, + } + + for _, td := range testData { + t.Run(td.testName, func(t *testing.T) { + if td.reqHandler != nil { + server := newMockHTTPServer(td.reqHandler) + if td.hostname == serverURL { + td.hostname = server.URL + } + defer server.Close() + } + c, err := New( + context.Background(), + td.hostname, + td.username, + td.password, + td.groupName, + td.verboseLogging, + td.authType, + td.opts) + if td.expectedErr != "" { + assert.ErrorContains(t, err, td.expectedErr) + } else { + assert.NoError(t, err) + assert.NotNil(t, c) + } + }) + } +} + +func TestDoAndGetResponseBody(t *testing.T) { + // Create a mock client + c := &client{ + hostname: "https://example.com", + http: http.DefaultClient, + } + ctx := context.Background() + + res := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(nil), + } + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + c.hostname = server.URL + headers := map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + } + orderedValues := [][][]byte{ + { + []byte("value1"), + []byte("value2"), + }, + } + res, _, err := c.DoAndGetResponseBody(ctx, http.MethodGet, "api/v1/endpoint", "", orderedValues, headers, EmptyMockBody{}) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + + body := &MockBody{ + ReadFunc: func(_ []byte) (n int, err error) { + return 0, io.EOF + }, + CloseFunc: func() error { + return nil + }, + } + res, _, _ = c.DoAndGetResponseBody(ctx, http.MethodGet, "api/v1/endpoint", "ID", orderedValues, headers, body) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func TestAuthenticate(t *testing.T) { + defaultDoAndGetResponseBodyFunc := doAndGetResponseBodyFunc + defer func() { + doAndGetResponseBodyFunc = defaultDoAndGetResponseBodyFunc + }() + + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + username := "testuser" + password := "testpassword" + endpoint := "https://example.com" + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/session/1/session/", r.URL.Path) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Authentication successful"}`)) + })) + defer server.Close() + c.hostname = server.URL + err := c.authenticate(ctx, username, password, endpoint) + assert.Equal(t, errors.New("authenticate error. response-"), err) + assert.Equal(t, "", c.GetReferer()) + + // Create a mock server for 201 response code + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/session/1/session/", r.URL.Path) + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"message":"Authentication successful"}`)) + })) + defer server.Close() + c.hostname = server.URL + _ = c.authenticate(ctx, username, password, endpoint) + assert.Equal(t, "", c.GetReferer()) + + // create a mock server for 401 response code + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/session/1/session/", r.URL.Path) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"message":"Authentication failed"}`)) + })) + defer server.Close() + c.hostname = server.URL + err = c.authenticate(ctx, username, password, endpoint) + assert.EqualError(t, err, "authentication failed. unable to login to powerscale. verify username and password") + + // force an error with a failed doAndGetResponseBody + doAndGetResponseBodyFunc = func( + _ *client, + _ context.Context, + _, _, _ string, + _ OrderedValues, _ map[string]string, + _ interface{}, + ) (*http.Response, bool, error) { + return nil, false, errors.New("failed doAndGetResponseBody") + } + err = c.authenticate(ctx, username, password, endpoint) + assert.EqualError(t, err, "Authentication error: failed doAndGetResponseBody") +} + +func TestExecuteWithRetryAuthenticate(t *testing.T) { + // error injection + defaultDoWithHeadersFunc := doWithHeadersFunc + defaultAuthenticateFunc := authenticateFunc + defer func() { + doWithHeadersFunc = defaultDoWithHeadersFunc + authenticateFunc = defaultAuthenticateFunc + }() + // Create a mock client + c := &client{ + http: http.DefaultClient, + authType: authTypeBasic, + username: "testuser", + password: "testpassword", + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + if strings.HasPrefix(r.URL.Path, "/bad-auth-") { + var res *JSONError + if strings.HasSuffix(r.URL.Path, "401/") { + res = &JSONError{StatusCode: http.StatusUnauthorized, Err: []Error{{Message: "Unauthorized", Code: "401"}}} + w.WriteHeader(http.StatusUnauthorized) + } else if strings.HasSuffix(r.URL.Path, "400/") { + res = &JSONError{StatusCode: http.StatusBadRequest, Err: []Error{{Message: "Bad Request", Code: "400"}}} + w.WriteHeader(http.StatusBadRequest) + } else { + res = &JSONError{StatusCode: http.StatusNotFound, Err: []Error{{Message: "Unknown URL", Code: "404"}}} + w.WriteHeader(http.StatusNotFound) + } + body, err := json.Marshal(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = w.Write(body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + return + } else if strings.HasPrefix(r.URL.Path, "/bad-html-auth-") { + var body string + w.Header().Set("Content-Type", "text/html") + if strings.HasSuffix(r.URL.Path, "401/") { + body = "HTML error 401 title" + w.WriteHeader(http.StatusUnauthorized) + } else if strings.HasSuffix(r.URL.Path, "400/") { + body = "HTML error 400 title" + w.WriteHeader(http.StatusBadRequest) + } else { + body = "HTML error title" + w.WriteHeader(http.StatusNotFound) + } + _, err := w.Write([]byte(body)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + return + } else if r.URL.Path == "/good-path/" { + w.WriteHeader(http.StatusOK) + return + } + } else if r.Method == http.MethodPost { + // Authentication successful + w.Header().Set(isiSessCsrfToken, "isisessid=123;isicsrf=abc;") + w.WriteHeader(http.StatusCreated) + return + } + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + c.hostname = server.URL + headers := map[string]string{ + "Content-Type": "text/html", + } + + err := c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/good-path", "", nil, headers, nil, nil) + assert.NoError(t, err) + + c.authType = authTypeSessionBased + + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/good-path", "", nil, headers, nil, nil) + assert.NoError(t, err) + + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-auth-401", "", nil, headers, nil, nil) + assert.Error(t, err) + + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-auth-400", "", nil, headers, nil, nil) + assert.Error(t, err) + + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-html-auth-401", "", nil, headers, nil, nil) + assert.Error(t, err) + + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-html-auth-400", "", nil, headers, nil, nil) + assert.Error(t, err) + + // force doWithHeaders to return unexpected error + doWithHeadersFunc = func(_ *client, _ context.Context, _ string, _ string, _ string, _ OrderedValues, _ map[string]string, _, _ interface{}) error { + return fmt.Errorf("mock error") + } + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-html-auth-400", "", nil, headers, nil, nil) + assert.Error(t, err) + doWithHeadersFunc = defaultDoWithHeadersFunc + + // force authenticate to return an error + authenticateFunc = func(_ *client, _ context.Context, _, _, _ string) error { + return errors.New("failed auth") + } + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-html-auth-401", "", nil, headers, nil, nil) + assert.Error(t, err) + err = c.executeWithRetryAuthenticate(ctx, http.MethodGet, "/bad-auth-401", "", nil, headers, nil, nil) + assert.Error(t, err) +} + +func TestDoWithHeaders(t *testing.T) { + // Create a mock client + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/api/v1/endpoint/", r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Success"}`)) + })) + defer server.Close() + c.hostname = server.URL + resp := &struct { + Message string `json:"message"` + }{} + + err := c.DoWithHeaders(ctx, http.MethodGet, "api/v1/endpoint", "", nil, nil, nil, resp) + assert.NoError(t, err) + expectedResp := &struct { + Message string `json:"message"` + }{ + Message: "Success", + } + assert.Equal(t, expectedResp, resp) +} + +func TestClient_APIVersion(t *testing.T) { + c := &client{apiVersion: 1} + assert.Equal(t, uint8(1), c.APIVersion()) +} + +func TestClient_User(t *testing.T) { + c := &client{username: "testuser"} + assert.Equal(t, "testuser", c.User()) +} + +func TestClient_Group(t *testing.T) { + c := &client{groupname: "testgroup"} + assert.Equal(t, "testgroup", c.Group()) +} + +func TestClient_VolumesPath(t *testing.T) { + c := &client{volumePath: "/mnt/volumes"} + assert.Equal(t, "/mnt/volumes", c.VolumesPath()) +} + +func TestClient_VolumePath(t *testing.T) { + c := &client{volumePath: "/mnt/volumes"} + assert.Equal(t, "/mnt/volumes/volume1", c.VolumePath("volume1")) +} + +func TestHTMLError_Error(t *testing.T) { + err := &HTMLError{Message: "HTML error message"} + assert.Equal(t, "HTML error message", err.Error()) +} + +func TestClient_SetAuthToken(t *testing.T) { + c := &client{} + c.SetAuthToken("testcookie") + assert.Equal(t, "testcookie", c.sessionCredentials.sessionCookies) +} + +func TestClient_SetCSRFToken(t *testing.T) { + c := &client{} + c.SetCSRFToken("testcsrf") + assert.Equal(t, "testcsrf", c.sessionCredentials.sessionCSRF) +} + +func TestClient_SetReferer(t *testing.T) { + c := &client{} + c.SetReferer("testreferer") + assert.Equal(t, "testreferer", c.sessionCredentials.referer) +} + +func TestClient_GetCSRFToken(t *testing.T) { + c := &client{} + c.GetCSRFToken() + assert.Equal(t, "", c.sessionCredentials.sessionCSRF) +} + +func TestParseJSONHTMLError(t *testing.T) { + tests := []struct { + name string + contentType string + body string + expectedErr error + expectedStatus int + }{ + { + name: "HTML error response", + contentType: "text/html", + body: `HTML error title

HTML error message

`, + expectedErr: &HTMLError{Message: "HTML error message"}, + expectedStatus: 401, + }, + { + name: "HTML error without h1", + contentType: "text/html", + body: `HTML error title`, + expectedErr: &HTMLError{Message: "HTML error title"}, + expectedStatus: 403, + }, + { + name: "Invalid JSON", + contentType: "application/json", + body: `{invalid json`, + expectedErr: &JSONError{Err: []Error{{Message: "invalid character 'i' looking for beginning of object key string"}}}, + expectedStatus: 400, + }, + { + name: "Invalid HTML", + contentType: "text/html", + body: ``, + expectedErr: &HTMLError{Message: ""}, + expectedStatus: 500, + }, + { + name: "JSON error with empty message", + contentType: "application/json", + body: `{"errors":[{"message":""}]}`, + expectedErr: &JSONError{Err: []Error{{Message: "400"}}}, + expectedStatus: 400, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + body := bytes.NewBufferString(tt.body) + resp := httptest.NewRecorder() + resp.Body = body + resp.Header().Set("Content-Type", tt.contentType) + resp.Code = tt.expectedStatus + + err := parseJSONHTMLError(resp.Result()) + + if tt.expectedErr != nil { + assert.NotNil(t, err) + + switch expected := tt.expectedErr.(type) { + case *JSONError: + assert.Contains(t, err.Error(), expected.Error()) + default: + assert.IsType(t, expected, err) + assert.EqualError(t, err, expected.Error()) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestClient_Put(t *testing.T) { + // Create a mock client + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodPut, r.Method) + assert.Equal(t, "/PUT/api/v1/endpoint", r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Success"}`)) + })) + defer server.Close() + c.hostname = server.URL + body := map[string]string{ + "Content-Type": "application/json", + } + resp := &struct { + Message string `json:"message"` + }{} + // Call the Put method + err := c.Put(ctx, http.MethodPut, "api/v1/endpoint", nil, nil, body, resp) + assert.NoError(t, err) +} + +func TestClient_Post(t *testing.T) { + // Create a mock client + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/POST/api/v1/endpoint", r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Success"}`)) + })) + defer server.Close() + c.hostname = server.URL + body := map[string]string{ + "Content-Type": "application/json", + } + resp := &struct { + Message string `json:"message"` + }{} + // Call the Post method + err := c.Post(ctx, http.MethodPost, "api/v1/endpoint", nil, nil, body, resp) + + // Assertions + assert.NoError(t, err) +} + +func TestClient_Delete(t *testing.T) { + // Create a mock client + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, http.MethodDelete, r.Method) + assert.Equal(t, "/DELETE/api/v1/endpoint", r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Success"}`)) + })) + defer server.Close() + c.hostname = server.URL + resp := &struct { + Message string `json:"message"` + }{} + // Call the Delete method + err := c.Delete(ctx, http.MethodDelete, "api/v1/endpoint", nil, nil, resp) + + // Assertions + assert.NoError(t, err) +} + +func TestClient_Do(t *testing.T) { + // Create a mock client + c := &client{ + http: http.DefaultClient, + } + ctx := context.Background() + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request is as expected + assert.Equal(t, "method", r.Method) + assert.Equal(t, "/api/v1/endpoint/", r.URL.String()) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message":"Success"}`)) + })) + defer server.Close() + c.hostname = server.URL + resp := &struct { + Message string `json:"message"` + }{} + // Call the Do method + err := c.Do(ctx, "method", "api/v1/endpoint", "", nil, resp, resp) + + // Assertions + assert.NoError(t, err) +} diff --git a/api/common/utils/poll_test.go b/api/common/utils/poll_test.go new file mode 100644 index 00000000..22608808 --- /dev/null +++ b/api/common/utils/poll_test.go @@ -0,0 +1,166 @@ +/* +Copyright (c) 2025 Dell Inc, or its subsidiaries. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPollImmediateWithContext_Success(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Condition function that returns true immediately + condition := func(context.Context) (bool, error) { + return true, nil + } + + err := PollImmediateWithContext(ctx, 1*time.Second, 3*time.Second, condition) + assert.NoError(t, err) +} + +func TestPollImmediateWithContext_Timeout(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + // Condition function that never returns true + condition := func(context.Context) (bool, error) { + return false, nil + } + + err := PollImmediateWithContext(ctx, 1*time.Second, 2*time.Second, condition) + assert.Error(t, err) + assert.Equal(t, ErrWaitTimeout, err) +} + +func TestWaitForWithContext_Success(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Wait function that uses a short interval + wait := func(ctx context.Context) <-chan struct{} { + ch := make(chan struct{}) + go func() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + ch <- struct{}{} + case <-ctx.Done(): + close(ch) + return + } + } + }() + return ch + } + + // Condition function that returns true after a short delay + condition := func(context.Context) (bool, error) { + time.Sleep(1 * time.Second) + return true, nil + } + + err := WaitForWithContext(ctx, wait, condition) + assert.NoError(t, err) +} + +func TestWaitForWithContext_Timeout(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Wait function that uses a short interval + wait := func(ctx context.Context) <-chan struct{} { + ch := make(chan struct{}) + go func() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + ch <- struct{}{} + case <-ctx.Done(): + close(ch) + return + } + } + }() + return ch + } + + // Condition function that never returns true + condition := func(context.Context) (bool, error) { + return false, nil + } + + err := WaitForWithContext(ctx, wait, condition) + assert.Error(t, err) + assert.Equal(t, ErrWaitTimeout, err) +} + +func TestPollImmediateWithContext_Error(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Condition function that returns an error + condition := func(context.Context) (bool, error) { + return false, errors.New("condition error") + } + + err := PollImmediateWithContext(ctx, 1*time.Second, 3*time.Second, condition) + assert.Error(t, err) + assert.Equal(t, "condition error", err.Error()) +} + +func TestWaitForWithContext_Error(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Wait function that uses a short interval + wait := func(ctx context.Context) <-chan struct{} { + ch := make(chan struct{}) + go func() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + ch <- struct{}{} + case <-ctx.Done(): + close(ch) + return + } + } + }() + return ch + } + + // Condition function that returns an error + condition := func(context.Context) (bool, error) { + return false, errors.New("condition error") + } + + err := WaitForWithContext(ctx, wait, condition) + assert.Error(t, err) + assert.Equal(t, "condition error", err.Error()) +} diff --git a/api/common/utils/utils_test.go b/api/common/utils/utils_test.go index 3f5b18e7..9f8fdeb4 100644 --- a/api/common/utils/utils_test.go +++ b/api/common/utils/utils_test.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2022 Dell Inc, or its subsidiaries. +Copyright (c) 2022-2025 Dell Inc, or its subsidiaries. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,16 +16,11 @@ limitations under the License. package utils import ( - "fmt" "testing" "github.com/stretchr/testify/assert" ) -func TestMain(_ *testing.M) { - fmt.Print("executing TestMain\n") -} - func TestIsStringInSlice(t *testing.T) { list := []string{"hello", "world", "jason"} @@ -34,6 +29,14 @@ func TestIsStringInSlice(t *testing.T) { assert.False(t, IsStringInSlice("harry", nil)) } +func TestIsStringInSlices(t *testing.T) { + list := []string{"hello", "world", "jason"} + + assert.True(t, IsStringInSlices("world", list)) + assert.False(t, IsStringInSlices("mary", list)) + assert.False(t, IsStringInSlices("harry", nil)) +} + func TestRemoveStringFromSlice(t *testing.T) { list := []string{"hello", "world", "jason"} diff --git a/api/json/json_decode.go b/api/json/json_decode.go deleted file mode 100644 index 8278e06c..00000000 --- a/api/json/json_decode.go +++ /dev/null @@ -1,1213 +0,0 @@ -// Copyright 2010 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. - -// Represents JSON data structure using native Go types: booleans, floats, -// strings, arrays, and maps. - -package json - -import ( - "bytes" - "encoding" - "encoding/base64" - "errors" - "fmt" - "reflect" - "runtime" - "strconv" - "unicode" - "unicode/utf16" - "unicode/utf8" -) - -// Unmarshal parses the JSON-encoded data and stores the result -// in the value pointed to by v. -// -// Unmarshal uses the inverse of the encodings that -// Marshal uses, allocating maps, slices, and pointers as necessary, -// with the following additional rules: -// -// To unmarshal JSON into a pointer, Unmarshal first handles the case of -// the JSON being the JSON literal null. In that case, Unmarshal sets -// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into -// the value pointed at by the pointer. If the pointer is nil, Unmarshal -// allocates a new value for it to point to. -// -// To unmarshal JSON into a struct, Unmarshal matches incoming object -// keys to the keys used by Marshal (either the struct field name or its tag), -// preferring an exact match but also accepting a case-insensitive match. -// Unmarshal will only set exported fields of the struct. -// -// To unmarshal JSON into an interface value, -// Unmarshal stores one of these in the interface value: -// -// bool, for JSON booleans -// float64, for JSON numbers -// string, for JSON strings -// []interface{}, for JSON arrays -// map[string]interface{}, for JSON objects -// nil for JSON null -// -// To unmarshal a JSON array into a slice, Unmarshal resets the slice length -// to zero and then appends each element to the slice. -// As a special case, to unmarshal an empty JSON array into a slice, -// Unmarshal replaces the slice with a new empty slice. -// -// To unmarshal a JSON array into a Go array, Unmarshal decodes -// JSON array elements into corresponding Go array elements. -// If the Go array is smaller than the JSON array, -// the additional JSON array elements are discarded. -// If the JSON array is smaller than the Go array, -// the additional Go array elements are set to zero values. -// -// To unmarshal a JSON object into a map, Unmarshal first establishes a map to -// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal -// reuses the existing map, keeping existing entries. Unmarshal then stores key- -// value pairs from the JSON object into the map. The map's key type must -// either be a string, an integer, or implement encoding.TextUnmarshaler. -// -// If a JSON value is not appropriate for a given target type, -// or if a JSON number overflows the target type, Unmarshal -// skips that field and completes the unmarshaling as best it can. -// If no more serious errors are encountered, Unmarshal returns -// an UnmarshalTypeError describing the earliest such error. -// -// The JSON null value unmarshals into an interface, map, pointer, or slice -// by setting that Go value to nil. Because null is often used in JSON to mean -// “not present,” unmarshaling a JSON null into any other Go type has no effect -// on the value and produces no error. -// -// When unmarshaling quoted strings, invalid UTF-8 or -// invalid UTF-16 surrogate pairs are not treated as an error. -// Instead, they are replaced by the Unicode replacement -// character U+FFFD. -func Unmarshal(data []byte, v interface{}) error { - // Check for well-formedness. - // Avoids filling out half a data structure - // before discovering a JSON syntax error. - var d decodeState - err := checkValid(data, &d.scan) - if err != nil { - return err - } - - d.init(data) - return d.unmarshal(v) -} - -// Unmarshaler is the interface implemented by types -// that can unmarshal a JSON description of themselves. -// The input can be assumed to be a valid encoding of -// a JSON value. UnmarshalJSON must copy the JSON data -// if it wishes to retain the data after returning. -type Unmarshaler interface { - UnmarshalJSON([]byte) error -} - -// An UnmarshalTypeError describes a JSON value that was -// not appropriate for a value of a specific Go type. -type UnmarshalTypeError struct { - Value string // description of JSON value - "bool", "array", "number -5" - Type reflect.Type // type of Go value it could not be assigned to - Offset int64 // error occurred after reading Offset bytes -} - -func (e *UnmarshalTypeError) Error() string { - return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() -} - -// An UnmarshalFieldError describes a JSON object key that -// led to an unexported (and therefore unwritable) struct field. -// (No longer used; kept for compatibility.) -type UnmarshalFieldError struct { - Key string - Type reflect.Type - Field reflect.StructField -} - -func (e *UnmarshalFieldError) Error() string { - return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() -} - -// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. -// (The argument to Unmarshal must be a non-nil pointer.) -type InvalidUnmarshalError struct { - Type reflect.Type -} - -func (e *InvalidUnmarshalError) Error() string { - if e.Type == nil { - return "json: Unmarshal(nil)" - } - - if e.Type.Kind() != reflect.Ptr { - return "json: Unmarshal(non-pointer " + e.Type.String() + ")" - } - return "json: Unmarshal(nil " + e.Type.String() + ")" -} - -func (d *decodeState) unmarshal(v interface{}) (err error) { - defer func() { - if r := recover(); r != nil { - if _, ok := r.(runtime.Error); ok { - panic(r) - } - err = r.(error) - } - }() - - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr || rv.IsNil() { - return &InvalidUnmarshalError{reflect.TypeOf(v)} - } - - d.scan.reset() - // We decode rv not rv.Elem because the Unmarshaler interface - // test must be applied at the top level of the value. - d.value(rv) - return d.savedError -} - -// A Number represents a JSON number literal. -type Number string - -// String returns the literal text of the number. -func (n Number) String() string { return string(n) } - -// Float64 returns the number as a float64. -func (n Number) Float64() (float64, error) { - return strconv.ParseFloat(string(n), 64) -} - -// Int64 returns the number as an int64. -func (n Number) Int64() (int64, error) { - return strconv.ParseInt(string(n), 10, 64) -} - -// isValidNumber reports whether s is a valid JSON number literal. -func isValidNumber(s string) bool { - // This function implements the JSON numbers grammar. - // See https://tools.ietf.org/html/rfc7159#section-6 - // and http://json.org/number.gif - - if s == "" { - return false - } - - // Optional - - if s[0] == '-' { - s = s[1:] - if s == "" { - return false - } - } - - // Digits - switch { - default: - return false - - case s[0] == '0': - s = s[1:] - - case '1' <= s[0] && s[0] <= '9': - s = s[1:] - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // . followed by 1 or more digits. - if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { - s = s[2:] - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // e or E followed by an optional - or + and - // 1 or more digits. - if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { - s = s[1:] - if s[0] == '+' || s[0] == '-' { - s = s[1:] - if s == "" { - return false - } - } - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // Make sure we are at the end. - return s == "" -} - -// decodeState represents the state while decoding a JSON value. -type decodeState struct { - data []byte - off int // read offset in data - scan scanner - nextscan scanner // for calls to nextValue - savedError error - useNumber bool -} - -// errPhase is used for errors that should not happen unless -// there is a bug in the JSON decoder or something is editing -// the data slice while the decoder executes. -var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") - -func (d *decodeState) init(data []byte) *decodeState { - d.data = data - d.off = 0 - d.savedError = nil - return d -} - -// error aborts the decoding by panicking with err. -func (d *decodeState) error(err error) { - panic(err) -} - -// saveError saves the first err it is called with, -// for reporting at the end of the unmarshal. -func (d *decodeState) saveError(err error) { - if d.savedError == nil { - d.savedError = err - } -} - -// next cuts off and returns the next full JSON value in d.data[d.off:]. -// The next value is known to be an object or array, not a literal. -func (d *decodeState) next() []byte { - c := d.data[d.off] - item, rest, err := nextValue(d.data[d.off:], &d.nextscan) - if err != nil { - d.error(err) - } - d.off = len(d.data) - len(rest) - - // Our scanner has seen the opening brace/bracket - // and thinks we're still in the middle of the object. - // invent a closing brace/bracket to get it out. - if c == '{' { - d.scan.step(&d.scan, '}') - } else { - d.scan.step(&d.scan, ']') - } - - return item -} - -// scanWhile processes bytes in d.data[d.off:] until it -// receives a scan code not equal to op. -// It updates d.off and returns the new scan code. -func (d *decodeState) scanWhile(op int) int { - var newOp int - for { - if d.off >= len(d.data) { - newOp = d.scan.eof() - d.off = len(d.data) + 1 // mark processed EOF with len+1 - } else { - c := d.data[d.off] - d.off++ - newOp = d.scan.step(&d.scan, c) - } - if newOp != op { - break - } - } - return newOp -} - -// value decodes a JSON value from d.data[d.off:] into the value. -// it updates d.off to point past the decoded value. -func (d *decodeState) value(v reflect.Value) { - if !v.IsValid() { - _, rest, err := nextValue(d.data[d.off:], &d.nextscan) - if err != nil { - d.error(err) - } - d.off = len(d.data) - len(rest) - - // d.scan thinks we're still at the beginning of the item. - // Feed in an empty string - the shortest, simplest value - - // so that it knows we got to the end of the value. - if d.scan.redo { - // rewind. - d.scan.redo = false - d.scan.step = stateBeginValue - } - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '"') - - n := len(d.scan.parseState) - if n > 0 && d.scan.parseState[n-1] == parseObjectKey { - // d.scan thinks we just read an object key; finish the object - d.scan.step(&d.scan, ':') - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '}') - } - - return - } - - switch op := d.scanWhile(scanSkipSpace); op { - default: - d.error(errPhase) - - case scanBeginArray: - d.array(v) - - case scanBeginObject: - d.object(v) - - case scanBeginLiteral: - d.literal(v) - } -} - -type unquotedValue struct{} - -// valueQuoted is like value but decodes a -// quoted string literal or literal null into an interface value. -// If it finds anything other than a quoted string literal or null, -// valueQuoted returns unquotedValue{}. -func (d *decodeState) valueQuoted() interface{} { - switch op := d.scanWhile(scanSkipSpace); op { - default: - d.error(errPhase) - - case scanBeginArray: - d.array(reflect.Value{}) - - case scanBeginObject: - d.object(reflect.Value{}) - - case scanBeginLiteral: - switch v := d.literalInterface().(type) { - case nil, string: - return v - } - } - return unquotedValue{} -} - -// indirect walks down v allocating pointers as needed, -// until it gets to a non-pointer. -// if it encounters an Unmarshaler, indirect stops and returns that. -// if decodingNull is true, indirect stops at the last pointer so it can be set to nil. -func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { - // If v is a named type and is addressable, - // start with its address, so that if the type has pointer methods, - // we find them. - if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { - v = v.Addr() - } - for { - // Load value from interface, but only if the result will be - // usefully addressable. - if v.Kind() == reflect.Interface && !v.IsNil() { - e := v.Elem() - if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { - v = e - continue - } - } - - if v.Kind() != reflect.Ptr { - break - } - - if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { - break - } - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - if v.Type().NumMethod() > 0 { - if u, ok := v.Interface().(Unmarshaler); ok { - return u, nil, reflect.Value{} - } - if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { - return nil, u, reflect.Value{} - } - } - v = v.Elem() - } - return nil, nil, v -} - -// array consumes an array from d.data[d.off-1:], decoding into the value v. -// the first byte of the array ('[') has been read already. -func (d *decodeState) array(v reflect.Value) { - // Check for unmarshaler. - u, ut, pv := d.indirect(v, false) - if u != nil { - d.off-- - err := u.UnmarshalJSON(d.next()) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) - d.off-- - d.next() - return - } - - v = pv - - // Check type of target. - switch v.Kind() { - case reflect.Interface: - if v.NumMethod() == 0 { - // Decoding into nil interface? Switch to non-reflect code. - v.Set(reflect.ValueOf(d.arrayInterface())) - return - } - // Otherwise it's invalid. - fallthrough - default: - d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) - d.off-- - d.next() - return - case reflect.Array: - case reflect.Slice: - break - } - - i := 0 - for { - // Look ahead for ] - can only happen on first iteration. - op := d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - - // Back up so d.value can have the byte we just read. - d.off-- - d.scan.undo(op) - - // Get element of array, growing if necessary. - if v.Kind() == reflect.Slice { - // Grow slice if necessary - if i >= v.Cap() { - newcap := v.Cap() + v.Cap()/2 - if newcap < 4 { - newcap = 4 - } - newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) - reflect.Copy(newv, v) - v.Set(newv) - } - if i >= v.Len() { - v.SetLen(i + 1) - } - } - - if i < v.Len() { - // Decode into element. - d.value(v.Index(i)) - } else { - // Ran out of fixed array: skip. - d.value(reflect.Value{}) - } - i++ - - // Next token must be , or ]. - op = d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - if op != scanArrayValue { - d.error(errPhase) - } - } - - if i < v.Len() { - if v.Kind() == reflect.Array { - // Array. Zero the rest. - z := reflect.Zero(v.Type().Elem()) - for ; i < v.Len(); i++ { - v.Index(i).Set(z) - } - } else { - v.SetLen(i) - } - } - if i == 0 && v.Kind() == reflect.Slice { - v.Set(reflect.MakeSlice(v.Type(), 0, 0)) - } -} - -var ( - nullLiteral = []byte("null") - textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() -) - -// object consumes an object from d.data[d.off-1:], decoding into the value v. -// the first byte ('{') of the object has been read already. -func (d *decodeState) object(v reflect.Value) { - // Check for unmarshaler. - u, ut, pv := d.indirect(v, false) - if u != nil { - d.off-- - err := u.UnmarshalJSON(d.next()) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - v = pv - - // Decoding into nil interface? Switch to non-reflect code. - if v.Kind() == reflect.Interface && v.NumMethod() == 0 { - v.Set(reflect.ValueOf(d.objectInterface())) - return - } - - // Check type of target: - // struct or - // map[T1]T2 where T1 is string, an integer type, - // or an encoding.TextUnmarshaler - switch v.Kind() { - case reflect.Map: - // Map key must either have string kind, have an integer kind, - // or be an encoding.TextUnmarshaler. - t := v.Type() - switch t.Key().Kind() { - case reflect.String, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - default: - if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) { - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - } - if v.IsNil() { - v.Set(reflect.MakeMap(t)) - } - case reflect.Struct: - - default: - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - - var mapElem reflect.Value - - for { - // Read opening " of string key or closing }. - op := d.scanWhile(scanSkipSpace) - if op == scanEndObject { - // closing } - can only happen on first iteration. - break - } - if op != scanBeginLiteral { - d.error(errPhase) - } - - // Read key. - start := d.off - 1 - op = d.scanWhile(scanContinue) - item := d.data[start : d.off-1] - key, ok := unquoteBytes(item) - if !ok { - d.error(errPhase) - } - - // Figure out field corresponding to key. - var subv reflect.Value - destring := false // whether the value is wrapped in a string to be decoded first - - if v.Kind() == reflect.Map { - elemType := v.Type().Elem() - if !mapElem.IsValid() { - mapElem = reflect.New(elemType).Elem() - } else { - mapElem.Set(reflect.Zero(elemType)) - } - subv = mapElem - } else { - var f *field - fields := cachedTypeFields(v.Type()) - for i := range fields { - if fields[i].omitUnmarshal { - continue - } - ff := &fields[i] - if bytes.Equal(ff.nameBytes, key) { - f = ff - break - } - if f == nil && ff.equalFold(ff.nameBytes, key) { - f = ff - } - } - if f != nil { - subv = v - destring = f.quoted - for _, i := range f.index { - if subv.Kind() == reflect.Ptr { - if subv.IsNil() { - subv.Set(reflect.New(subv.Type().Elem())) - } - subv = subv.Elem() - } - subv = subv.Field(i) - } - } - } - - // Read : before value. - if op == scanSkipSpace { - op = d.scanWhile(scanSkipSpace) - } - if op != scanObjectKey { - d.error(errPhase) - } - - // Read value. - if destring { - switch qv := d.valueQuoted().(type) { - case nil: - d.literalStore(nullLiteral, subv, false) - case string: - d.literalStore([]byte(qv), subv, true) - default: - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) - } - } else { - d.value(subv) - } - - // Write value back to map; - // if using struct, subv points into struct already. - if v.Kind() == reflect.Map { - kt := v.Type().Key() - var kv reflect.Value - switch { - case kt.Kind() == reflect.String: - kv = reflect.ValueOf(key).Convert(kt) - case reflect.PtrTo(kt).Implements(textUnmarshalerType): - kv = reflect.New(v.Type().Key()) - d.literalStore(item, kv, true) - kv = kv.Elem() - default: - switch kt.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s := string(key) - n, err := strconv.ParseInt(s, 10, 64) - if err != nil || reflect.Zero(kt).OverflowInt(n) { - d.saveError(&UnmarshalTypeError{"number " + s, kt, int64(start + 1)}) - return - } - kv = reflect.ValueOf(n).Convert(kt) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s := string(key) - n, err := strconv.ParseUint(s, 10, 64) - if err != nil || reflect.Zero(kt).OverflowUint(n) { - d.saveError(&UnmarshalTypeError{"number " + s, kt, int64(start + 1)}) - return - } - kv = reflect.ValueOf(n).Convert(kt) - default: - panic("json: Unexpected key type") // should never occur - } - } - v.SetMapIndex(kv, subv) - } - - // Next token must be , or }. - op = d.scanWhile(scanSkipSpace) - if op == scanEndObject { - break - } - if op != scanObjectValue { - d.error(errPhase) - } - } -} - -// literal consumes a literal from d.data[d.off-1:], decoding into the value v. -// The first byte of the literal has been read already -// (that's how the caller knows it's a literal). -func (d *decodeState) literal(v reflect.Value) { - // All bytes inside literal return scanContinue op code. - start := d.off - 1 - op := d.scanWhile(scanContinue) - - // Scan read one byte too far; back up. - d.off-- - d.scan.undo(op) - - d.literalStore(d.data[start:d.off], v, false) -} - -// convertNumber converts the number literal s to a float64 or a Number -// depending on the setting of d.useNumber. -func (d *decodeState) convertNumber(s string) (interface{}, error) { - if d.useNumber { - return Number(s), nil - } - f, err := strconv.ParseFloat(s, 64) - if err != nil { - return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} - } - return f, nil -} - -var numberType = reflect.TypeOf(Number("")) - -// literalStore decodes a literal stored in item into v. -// -// fromQuoted indicates whether this literal came from unwrapping a -// string from the ",string" struct tag option. this is used only to -// produce more helpful error messages. -func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { - // Check for unmarshaler. - if len(item) == 0 { - // Empty string given - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - return - } - wantptr := item[0] == 'n' // null - u, ut, pv := d.indirect(v, wantptr) - if u != nil { - err := u.UnmarshalJSON(item) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - if item[0] != '"' { - if fromQuoted { - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - } - return - } - s, ok := unquoteBytes(item) - if !ok { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - err := ut.UnmarshalText(s) - if err != nil { - d.error(err) - } - return - } - - v = pv - - switch c := item[0]; c { - case 'n': // null - switch v.Kind() { - case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: - v.Set(reflect.Zero(v.Type())) - // otherwise, ignore null for primitives/string - } - case 't', 'f': // true, false - value := c == 't' - switch v.Kind() { - default: - if fromQuoted { - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) - } - case reflect.Bool: - v.SetBool(value) - case reflect.Interface: - if v.NumMethod() == 0 { - v.Set(reflect.ValueOf(value)) - } else { - d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) - } - } - - case '"': // string - s, ok := unquoteBytes(item) - if !ok { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - switch v.Kind() { - default: - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - case reflect.Slice: - if v.Type().Elem().Kind() != reflect.Uint8 { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - break - } - b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) - n, err := base64.StdEncoding.Decode(b, s) - if err != nil { - d.saveError(err) - break - } - v.SetBytes(b[:n]) - case reflect.String: - v.SetString(string(s)) - case reflect.Interface: - if v.NumMethod() == 0 { - v.Set(reflect.ValueOf(string(s))) - } else { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - } - } - - default: // number - if c != '-' && (c < '0' || c > '9') { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - s := string(item) - switch v.Kind() { - default: - if v.Kind() == reflect.String && v.Type() == numberType { - v.SetString(s) - if !isValidNumber(s) { - d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) - } - break - } - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) - } - case reflect.Interface: - n, err := d.convertNumber(s) - if err != nil { - d.saveError(err) - break - } - if v.NumMethod() != 0 { - d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) - break - } - v.Set(reflect.ValueOf(n)) - - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - n, err := strconv.ParseInt(s, 10, 64) - if err != nil || v.OverflowInt(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetInt(n) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - n, err := strconv.ParseUint(s, 10, 64) - if err != nil || v.OverflowUint(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetUint(n) - - case reflect.Float32, reflect.Float64: - n, err := strconv.ParseFloat(s, v.Type().Bits()) - if err != nil || v.OverflowFloat(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetFloat(n) - } - } -} - -// The xxxInterface routines build up a value to be stored -// in an empty interface. They are not strictly necessary, -// but they avoid the weight of reflection in this common case. - -// valueInterface is like value but returns interface{} -func (d *decodeState) valueInterface() interface{} { - switch d.scanWhile(scanSkipSpace) { - default: - d.error(errPhase) - panic("unreachable") - case scanBeginArray: - return d.arrayInterface() - case scanBeginObject: - return d.objectInterface() - case scanBeginLiteral: - return d.literalInterface() - } -} - -// arrayInterface is like array but returns []interface{}. -func (d *decodeState) arrayInterface() []interface{} { - v := make([]interface{}, 0) - for { - // Look ahead for ] - can only happen on first iteration. - op := d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - - // Back up so d.value can have the byte we just read. - d.off-- - d.scan.undo(op) - - v = append(v, d.valueInterface()) - - // Next token must be , or ]. - op = d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - if op != scanArrayValue { - d.error(errPhase) - } - } - return v -} - -// objectInterface is like object but returns map[string]interface{}. -func (d *decodeState) objectInterface() map[string]interface{} { - m := make(map[string]interface{}) - for { - // Read opening " of string key or closing }. - op := d.scanWhile(scanSkipSpace) - if op == scanEndObject { - // closing } - can only happen on first iteration. - break - } - if op != scanBeginLiteral { - d.error(errPhase) - } - - // Read string key. - start := d.off - 1 - op = d.scanWhile(scanContinue) - item := d.data[start : d.off-1] - key, ok := unquote(item) - if !ok { - d.error(errPhase) - } - - // Read : before value. - if op == scanSkipSpace { - op = d.scanWhile(scanSkipSpace) - } - if op != scanObjectKey { - d.error(errPhase) - } - - // Read value. - m[key] = d.valueInterface() - - // Next token must be , or }. - op = d.scanWhile(scanSkipSpace) - if op == scanEndObject { - break - } - if op != scanObjectValue { - d.error(errPhase) - } - } - return m -} - -// literalInterface is like literal but returns an interface value. -func (d *decodeState) literalInterface() interface{} { - // All bytes inside literal return scanContinue op code. - start := d.off - 1 - op := d.scanWhile(scanContinue) - - // Scan read one byte too far; back up. - d.off-- - d.scan.undo(op) - item := d.data[start:d.off] - - switch c := item[0]; c { - case 'n': // null - return nil - - case 't', 'f': // true, false - return c == 't' - - case '"': // string - s, ok := unquote(item) - if !ok { - d.error(errPhase) - } - return s - - default: // number - if c != '-' && (c < '0' || c > '9') { - d.error(errPhase) - } - n, err := d.convertNumber(string(item)) - if err != nil { - d.saveError(err) - } - return n - } -} - -// getu4 decodes \uXXXX from the beginning of s, returning the hex value, -// or it returns -1. -func getu4(s []byte) rune { - if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { - return -1 - } - r, err := strconv.ParseUint(string(s[2:6]), 16, 64) - if err != nil { - return -1 - } - return rune(r) -} - -// unquote converts a quoted JSON string literal s into an actual string t. -// The rules are different than for Go, so cannot use strconv.Unquote. -func unquote(s []byte) (t string, ok bool) { - s, ok = unquoteBytes(s) - t = string(s) - return -} - -func unquoteBytes(s []byte) (t []byte, ok bool) { - if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { - return - } - s = s[1 : len(s)-1] - - // Check for unusual characters. If there are none, - // then no unquoting is needed, so return a slice of the - // original bytes. - r := 0 - for r < len(s) { - c := s[r] - if c == '\\' || c == '"' || c < ' ' { - break - } - if c < utf8.RuneSelf { - r++ - continue - } - rr, size := utf8.DecodeRune(s[r:]) - if rr == utf8.RuneError && size == 1 { - break - } - r += size - } - if r == len(s) { - return s, true - } - - b := make([]byte, len(s)+2*utf8.UTFMax) - w := copy(b, s[0:r]) - for r < len(s) { - // Out of room? Can only happen if s is full of - // malformed UTF-8 and we're replacing each - // byte with RuneError. - if w >= len(b)-2*utf8.UTFMax { - nb := make([]byte, (len(b)+utf8.UTFMax)*2) - copy(nb, b[0:w]) - b = nb - } - switch c := s[r]; { - case c == '\\': - r++ - if r >= len(s) { - return - } - switch s[r] { - default: - return - case '"', '\\', '/', '\'': - b[w] = s[r] - r++ - w++ - case 'b': - b[w] = '\b' - r++ - w++ - case 'f': - b[w] = '\f' - r++ - w++ - case 'n': - b[w] = '\n' - r++ - w++ - case 'r': - b[w] = '\r' - r++ - w++ - case 't': - b[w] = '\t' - r++ - w++ - case 'u': - r-- - rr := getu4(s[r:]) - if rr < 0 { - return - } - r += 6 - if utf16.IsSurrogate(rr) { - rr1 := getu4(s[r:]) - if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { - // A valid pair; consume. - r += 6 - w += utf8.EncodeRune(b[w:], dec) - break - } - // Invalid surrogate; fall back to replacement rune. - rr = unicode.ReplacementChar - } - w += utf8.EncodeRune(b[w:], rr) - } - - // Quote, control characters are invalid. - case c == '"', c < ' ': - return - - // ASCII - case c < utf8.RuneSelf: - b[w] = c - r++ - w++ - - // Coerce to well-formed UTF-8. - default: - rr, size := utf8.DecodeRune(s[r:]) - r += size - w += utf8.EncodeRune(b[w:], rr) - } - } - return b[0:w], true -} diff --git a/api/json/json_encode.go b/api/json/json_encode.go deleted file mode 100644 index d4b1b0a6..00000000 --- a/api/json/json_encode.go +++ /dev/null @@ -1,1271 +0,0 @@ -// Copyright 2010 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 json implements encoding and decoding of JSON as defined in -// RFC 4627. The mapping between JSON and Go values is described -// in the documentation for the Marshal and Unmarshal functions. -// -// See "JSON and Go" for an introduction to this package: -// https://golang.org/doc/articles/json_and_go.html -package json - -import ( - "bytes" - "encoding" - "encoding/base64" - "fmt" - "math" - "reflect" - "runtime" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "unicode" - "unicode/utf8" -) - -// Marshal returns the JSON encoding of v. -// -// Marshal traverses the value v recursively. -// If an encountered value implements the Marshaler interface -// and is not a nil pointer, Marshal calls its MarshalJSON method -// to produce JSON. If no MarshalJSON method is present but the -// value implements encoding.TextMarshaler instead, Marshal calls -// its MarshalText method. -// The nil pointer exception is not strictly necessary -// but mimics a similar, necessary exception in the behavior of -// UnmarshalJSON. -// -// Otherwise, Marshal uses the following type-dependent default encodings: -// -// Boolean values encode as JSON booleans. -// -// Floating point, integer, and Number values encode as JSON numbers. -// -// String values encode as JSON strings coerced to valid UTF-8, -// replacing invalid bytes with the Unicode replacement rune. -// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" -// to keep some browsers from misinterpreting JSON output as HTML. -// Ampersand "&" is also escaped to "\u0026" for the same reason. -// This escaping can be disabled using an Encoder with DisableHTMLEscaping. -// -// Array and slice values encode as JSON arrays, except that -// []byte encodes as a base64-encoded string, and a nil slice -// encodes as the null JSON value. -// -// Struct values encode as JSON objects. Each exported struct field -// becomes a member of the object unless -// - the field's tag is "-", or -// - the field is empty and its tag specifies the "omitempty" option. -// -// The empty values are false, 0, any -// nil pointer or interface value, and any array, slice, map, or string of -// length zero. The object's default key string is the struct field name -// but can be specified in the struct field's tag value. The "json" key in -// the struct field's tag value is the key name, followed by an optional comma -// and options. Examples: -// -// // Field is ignored by this package. -// Field int `json:"-"` -// -// // Field appears in JSON as key "myName". -// Field int `json:"myName"` -// -// // Field appears in JSON as key "myName" and -// // the field is omitted from the object if its value is empty, -// // as defined above. -// Field int `json:"myName,omitempty"` -// -// // Field appears in JSON as key "Field" (the default), but -// // the field is skipped if empty. -// // Note the leading comma. -// Field int `json:",omitempty"` -// -// The "string" option signals that a field is stored as JSON inside a -// JSON-encoded string. It applies only to fields of string, floating point, -// integer, or boolean types. This extra level of encoding is sometimes used -// when communicating with JavaScript programs: -// -// Int64String int64 `json:",string"` -// -// The key name will be used if it's a non-empty string consisting of -// only Unicode letters, digits, and ASCII punctuation except quotation -// marks, backslash, and comma. -// -// Anonymous struct fields are usually marshaled as if their inner exported fields -// were fields in the outer struct, subject to the usual Go visibility rules amended -// as described in the next paragraph. -// An anonymous struct field with a name given in its JSON tag is treated as -// having that name, rather than being anonymous. -// An anonymous struct field of interface type is treated the same as having -// that type as its name, rather than being anonymous. -// -// The Go visibility rules for struct fields are amended for JSON when -// deciding which field to marshal or unmarshal. If there are -// multiple fields at the same level, and that level is the least -// nested (and would therefore be the nesting level selected by the -// usual Go rules), the following extra rules apply: -// -// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, -// even if there are multiple untagged fields that would otherwise conflict. -// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. -// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. -// -// Handling of anonymous struct fields is new in Go 1.1. -// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of -// an anonymous struct field in both current and earlier versions, give the field -// a JSON tag of "-". -// -// Map values encode as JSON objects. The map's key type must either be a -// string, an integer type, or implement encoding.TextMarshaler. The map keys -// are sorted and used as JSON object keys by applying the following rules, -// subject to the UTF-8 coercion described for string values above: -// - string keys are used directly -// - encoding.TextMarshalers are marshaled -// - integer keys are converted to strings -// -// Pointer values encode as the value pointed to. -// A nil pointer encodes as the null JSON value. -// -// Interface values encode as the value contained in the interface. -// A nil interface value encodes as the null JSON value. -// -// Channel, complex, and function values cannot be encoded in JSON. -// Attempting to encode such a value causes Marshal to return -// an UnsupportedTypeError. -// -// JSON cannot represent cyclic data structures and Marshal does not -// handle them. Passing cyclic structures to Marshal will result in -// an infinite recursion. -func Marshal(v interface{}) ([]byte, error) { - e := &encodeState{} - err := e.marshal(v, encOpts{escapeHTML: true}) - if err != nil { - return nil, err - } - return e.Bytes(), nil -} - -// MarshalIndent is like Marshal but applies Indent to format the output. -func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { - b, err := Marshal(v) - if err != nil { - return nil, err - } - var buf bytes.Buffer - err = Indent(&buf, b, prefix, indent) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 -// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 -// so that the JSON will be safe to embed inside HTML