Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
28
pkg/utils/app/app.go
Normal file
28
pkg/utils/app/app.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
)
|
||||
|
||||
// Setup primes application while enabling proper
|
||||
// cleanup methods
|
||||
func Setup(app string, cleanup func()) {
|
||||
logger.Info().Msgf("initializing %s", app)
|
||||
EnableGracefulShutdown(cleanup)
|
||||
}
|
||||
|
||||
// EnableGracefulShutdown catches ctrl+c interrupt
|
||||
// to allow for graceful shutdowns
|
||||
func EnableGracefulShutdown(cleanup func()) {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
cleanup()
|
||||
os.Exit(1)
|
||||
}()
|
||||
}
|
||||
38
pkg/utils/auth_helper.go
Normal file
38
pkg/utils/auth_helper.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
c "fiskerinc.com/modules/common/context"
|
||||
)
|
||||
|
||||
// auth_helper gives tool to our authorization services so that future middleware components and actual endpoints will have access
|
||||
// to user data needed for specific granular control
|
||||
|
||||
func AUTHWriteProviderToRequest(provider string, r *http.Request) *http.Request {
|
||||
newCTX := AUTHWriteProviderToContext(provider, r.Context())
|
||||
|
||||
r = r.Clone(newCTX)
|
||||
return r
|
||||
}
|
||||
|
||||
func AUTHWriteProviderToContext(provider string, ctx context.Context) (newCTX context.Context) {
|
||||
newCTX = context.WithValue(ctx, c.ProviderKey, provider)
|
||||
return newCTX
|
||||
}
|
||||
|
||||
// If provider is not in context, will return an empty string
|
||||
func AUTHGetProviderFromRequest(r *http.Request) string {
|
||||
return AUTHGetProviderFromContext(r.Context())
|
||||
}
|
||||
|
||||
// If provider is not in context, will return an empty string
|
||||
func AUTHGetProviderFromContext(ctx context.Context) string {
|
||||
val, ok := ctx.Value(c.ProviderKey).(string)
|
||||
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
45
pkg/utils/auth_helper_test.go
Normal file
45
pkg/utils/auth_helper_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/utils"
|
||||
)
|
||||
|
||||
func TestWriteProviderToContext(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
res := utils.AUTHGetProviderFromContext(ctx)
|
||||
if res != "" {
|
||||
t.Errorf("Expected context to not have a value under provider")
|
||||
}
|
||||
|
||||
provider := "testProvider"
|
||||
newCTX := utils.AUTHWriteProviderToContext(provider, ctx)
|
||||
res = utils.AUTHGetProviderFromContext(newCTX)
|
||||
if res != provider {
|
||||
t.Errorf("Expected: %s but got: %s", provider, res)
|
||||
}
|
||||
|
||||
res = utils.AUTHGetProviderFromContext(ctx)
|
||||
if res != "" {
|
||||
t.Errorf("Expected context to not change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteProviderToRequest(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
provider := "testProvider"
|
||||
|
||||
r = utils.AUTHWriteProviderToRequest(provider, r)
|
||||
|
||||
res := utils.AUTHGetProviderFromRequest(r)
|
||||
if res != provider {
|
||||
t.Errorf("Expected: %s but got: %s", provider, res)
|
||||
}
|
||||
}
|
||||
13
pkg/utils/bytearray/bytearray.go
Normal file
13
pkg/utils/bytearray/bytearray.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package bytearray
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
func FromUInt64(input uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, input)
|
||||
return b
|
||||
}
|
||||
|
||||
func ToUInt64(input []byte) uint64 {
|
||||
return binary.LittleEndian.Uint64(input)
|
||||
}
|
||||
57
pkg/utils/certificate_parser.go
Normal file
57
pkg/utils/certificate_parser.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/validator"
|
||||
)
|
||||
|
||||
// ParseVINFromRequest retrieves VIN from "Ssl-Client-Subject-Dn"
|
||||
//
|
||||
// which is generated by NGINX through a cert on websocket request
|
||||
func ParseVINFromRequest(r *http.Request) (vin string, err error) {
|
||||
// Try SDK 1.2 format first: Ssl-Client-Subject-Dn header
|
||||
vin = retrieveCommonNameFromHeaderSSLClientSubject(r)
|
||||
|
||||
if vin == "" {
|
||||
// Try SDK 1.4 format: Ssl header
|
||||
vin = retrieveCommonNameFromHeaderSSL(r)
|
||||
}
|
||||
|
||||
ok := validator.ValidateVINSimple(vin)
|
||||
if ok {
|
||||
return vin, nil
|
||||
}
|
||||
// If neither header format contains a VIN, return error
|
||||
return "", fmt.Errorf("VIN not found in request headers: '%s' bad value", vin)
|
||||
}
|
||||
|
||||
func retrieveCommonNameFromHeaderSSL(r *http.Request) (vin string) {
|
||||
sslHeader := r.Header.Get("Ssl")
|
||||
logger.Info().Str("SSL Header Value", sslHeader).Msg("CSDK 1.2 VIN Format Failed, trying 1.4")
|
||||
if sslHeader != "" {
|
||||
// Extract VIN from "CN=VIN_HERE" format
|
||||
if strings.HasPrefix(sslHeader, "CN=") {
|
||||
vin = strings.TrimPrefix(sslHeader, "CN=")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func retrieveCommonNameFromHeaderSSLClientSubject(r *http.Request) string {
|
||||
dn := r.Header.Values("Ssl-Client-Subject-Dn")
|
||||
|
||||
for _, d := range dn {
|
||||
fields := strings.Split(d, ",")
|
||||
for _, field := range fields {
|
||||
if len(field) > 3 && field[:3] == "CN=" {
|
||||
return field[3:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
137
pkg/utils/certificate_parser_test.go
Normal file
137
pkg/utils/certificate_parser_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/utils"
|
||||
)
|
||||
|
||||
func TestParseVINFromRequestBothFormats(t *testing.T) {
|
||||
testVIN := "1F15K3R45N1234567"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
headerName string
|
||||
headerValue string
|
||||
expectedVIN string
|
||||
expectedError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "SDK 1.2 format - Ssl-Client-Subject-Dn",
|
||||
headerName: "Ssl-Client-Subject-Dn",
|
||||
headerValue: "CN=" + testVIN,
|
||||
expectedVIN: testVIN,
|
||||
expectedError: false,
|
||||
description: "Should parse VIN from SDK 1.2 Ssl-Client-Subject-Dn header",
|
||||
},
|
||||
{
|
||||
name: "SDK 1.4 format - Ssl header",
|
||||
headerName: "Ssl",
|
||||
headerValue: "CN=" + testVIN,
|
||||
expectedVIN: testVIN,
|
||||
expectedError: false,
|
||||
description: "Should parse VIN from SDK 1.4 Ssl header",
|
||||
},
|
||||
{
|
||||
name: "Both headers present - should prefer SDK 1.2",
|
||||
headerName: "both",
|
||||
headerValue: "CN=" + testVIN,
|
||||
expectedVIN: testVIN,
|
||||
expectedError: false,
|
||||
description: "When both headers present, should use SDK 1.2 format first",
|
||||
},
|
||||
{
|
||||
name: "No VIN headers present",
|
||||
headerName: "none",
|
||||
headerValue: "",
|
||||
expectedVIN: "",
|
||||
expectedError: true,
|
||||
description: "Should return error when no VIN headers are present",
|
||||
},
|
||||
{
|
||||
name: "Invalid header format - no CN prefix",
|
||||
headerName: "Ssl-Client-Subject-Dn",
|
||||
headerValue: testVIN, // Missing CN= prefix
|
||||
expectedVIN: "",
|
||||
expectedError: true,
|
||||
description: "Should return error when header doesn't have CN= prefix",
|
||||
},
|
||||
{
|
||||
name: "Empty CN value",
|
||||
headerName: "Ssl",
|
||||
headerValue: "CN=",
|
||||
expectedVIN: "",
|
||||
expectedError: true,
|
||||
description: "Should return error when CN value is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create HTTP request
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
|
||||
// Set headers based on test case
|
||||
switch tt.headerName {
|
||||
case "Ssl-Client-Subject-Dn":
|
||||
req.Header.Set("Ssl-Client-Subject-Dn", tt.headerValue)
|
||||
case "Ssl":
|
||||
req.Header.Set("Ssl", tt.headerValue)
|
||||
case "both":
|
||||
req.Header.Set("Ssl-Client-Subject-Dn", tt.headerValue)
|
||||
req.Header.Set("Ssl", "CN=DIFFERENT_VIN") // Should not be used
|
||||
case "none":
|
||||
// Don't set any headers
|
||||
}
|
||||
|
||||
// Call function under test
|
||||
vin, err := utils.ParseVINFromRequest(req)
|
||||
|
||||
// Verify results
|
||||
if tt.expectedError {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error but got none", tt.description)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tt.description, err)
|
||||
}
|
||||
if vin != tt.expectedVIN {
|
||||
t.Errorf("%s: expected VIN %s, got %s", tt.description, tt.expectedVIN, vin)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test backward compatibility - existing SDK 1.2 test should still pass
|
||||
func TestParseVINFromRequestLegacy(t *testing.T) {
|
||||
const subjectDNHeader = "Ssl-Client-Subject-Dn"
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set(subjectDNHeader, "CN=1F15K3R45N1234567")
|
||||
|
||||
vin, err := utils.ParseVINFromRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("TestParseVINFromRequestLegacy: unexpected error: %v", err)
|
||||
}
|
||||
if vin != "1F15K3R45N1234567" {
|
||||
t.Errorf("TestParseVINFromRequestLegacy: expected VIN 1F15K3R45N1234567, got %s", vin)
|
||||
}
|
||||
}
|
||||
|
||||
// Test SDK 1.4 specific format
|
||||
func TestParseVINFromRequestSDK14(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Ssl", "CN=1F15K3R45N1234567")
|
||||
|
||||
vin, err := utils.ParseVINFromRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("TestParseVINFromRequestSDK14: unexpected error: %v", err)
|
||||
}
|
||||
if vin != "1F15K3R45N1234567" {
|
||||
t.Errorf("TestParseVINFromRequestSDK14: expected VIN 1F15K3R45N1234567, got %s", vin)
|
||||
}
|
||||
}
|
||||
5
pkg/utils/elptr/elptr.go
Normal file
5
pkg/utils/elptr/elptr.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package elptr
|
||||
|
||||
func ElPtr[T any](e T) *T {
|
||||
return &e
|
||||
}
|
||||
64
pkg/utils/envtool/env.go
Normal file
64
pkg/utils/envtool/env.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package envtool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetEnv return enviroment variable or default value if not set
|
||||
func GetEnv(name string, defaultValue string) string {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if ok && len(value) > 0 {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvInt returns environment variable or default value if not set
|
||||
func GetEnvInt(name string, defaultValue int) int {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if ok && len(value) > 0 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvInt returns environment variable or default value if not set
|
||||
func GetEnvInt64(name string, defaultValue int64) int64 {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if ok && len(value) > 0 {
|
||||
i, err := strconv.ParseInt(value, 10, 0)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetEnvBool returns environment variable or default value if not set
|
||||
func GetEnvBool(name string, defaultValue bool) bool {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
val, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func GetEnvDuration(name string, defaultValue time.Duration) time.Duration {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if ok && len(value) > 0 {
|
||||
d, err := time.ParseDuration(value)
|
||||
if err == nil {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
66
pkg/utils/envtool/env_test.go
Normal file
66
pkg/utils/envtool/env_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package envtool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
var defaultValue = "DEFAULT_VALUE"
|
||||
var existingValue = "EXISTING_VALUE"
|
||||
|
||||
var defaultInt = 0
|
||||
var existingInt = 1
|
||||
|
||||
var defaultBool = false
|
||||
var existingBool = true
|
||||
|
||||
func TestGetEnvDefaultValue(t *testing.T) {
|
||||
got := GetEnv("NON_EXISTING", defaultValue)
|
||||
if got != defaultValue {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Default value", defaultValue, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvExistingValue(t *testing.T) {
|
||||
const envName = "EXISTING"
|
||||
os.Setenv(envName, existingValue)
|
||||
got := GetEnv(envName, defaultValue)
|
||||
if got != existingValue {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Existing value", existingValue, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvIntDefaultValue(t *testing.T) {
|
||||
got := GetEnvInt("NON_EXISTING", defaultInt)
|
||||
if got != defaultInt {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Default value", defaultInt, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvIntExistingValue(t *testing.T) {
|
||||
const envName = "EXISTING"
|
||||
os.Setenv(envName, strconv.Itoa(existingInt))
|
||||
got := GetEnvInt(envName, defaultInt)
|
||||
if got != existingInt {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Existing value", existingInt, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvBoolDefaultValue(t *testing.T) {
|
||||
got := GetEnvBool("NON_EXISTING", defaultBool)
|
||||
if got != defaultBool {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Default Value", defaultBool, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvBoolExistingValue(t *testing.T) {
|
||||
const envName = "EXISTING"
|
||||
os.Setenv(envName, strconv.FormatBool(existingBool))
|
||||
got := GetEnvBool(envName, defaultBool)
|
||||
if got != existingBool {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Default Value", existingBool, got)
|
||||
}
|
||||
}
|
||||
11
pkg/utils/headerhelper.go
Normal file
11
pkg/utils/headerhelper.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
import "net/textproto"
|
||||
|
||||
func GetHTTPHeader(header textproto.MIMEHeader, name string, defaultValue string) string {
|
||||
if value, ok := header[name]; ok && len(value) > 0 {
|
||||
return value[0]
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
16
pkg/utils/hexadecimal.go
Normal file
16
pkg/utils/hexadecimal.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// HexToUInt32 converts a hexadecimal to uint32
|
||||
func HexToUInt32(hexadecimal string) (uint32, error) {
|
||||
id, err := strconv.ParseUint(hexadecimal, 16, 32)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
return uint32(id), nil
|
||||
}
|
||||
27
pkg/utils/hexadecimal_test.go
Normal file
27
pkg/utils/hexadecimal_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
th "fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestHexadecimal(t *testing.T) {
|
||||
hexCanID := "151"
|
||||
id, err := HexToUInt32(hexCanID)
|
||||
if err != nil {
|
||||
t.Errorf(th.TestErrorTemplate, "TestHexadecimal", nil, err)
|
||||
}
|
||||
|
||||
if id != 337 {
|
||||
t.Errorf(th.TestErrorTemplate, "TestHexadecimal", 337, id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHexadecimalError(t *testing.T) {
|
||||
hexCanID := "FISKER"
|
||||
_, err := HexToUInt32(hexCanID)
|
||||
if err == nil {
|
||||
t.Errorf(th.TestErrorTemplate, "TestHexadecimal", err, nil)
|
||||
}
|
||||
}
|
||||
63
pkg/utils/json_resp.go
Normal file
63
pkg/utils/json_resp.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
)
|
||||
|
||||
// LinkResult makes result with link and timestamp
|
||||
func LinkResult(link string, timestamp int64) common.JSONLink {
|
||||
return common.JSONLink{
|
||||
Link: link,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResult makes error result
|
||||
func ErrorResult(status int, message string) common.JSONError {
|
||||
return common.JSONError{
|
||||
Error: http.StatusText(status),
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// RespJSON sends back JSON response
|
||||
func RespJSON(w http.ResponseWriter, status int, resp interface{}) {
|
||||
js, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// CORS headers for local debugging
|
||||
/*
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "*")
|
||||
*/
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
// RespLink JSON response with download link
|
||||
func RespLink(w http.ResponseWriter, link string, timestamp int64) {
|
||||
resp := LinkResult(link, timestamp)
|
||||
RespJSON(w, http.StatusOK, &resp)
|
||||
}
|
||||
|
||||
// RespError JSON error response
|
||||
func RespError(w http.ResponseWriter, status int, message string) {
|
||||
resp := ErrorResult(status, message)
|
||||
RespJSON(w, status, &resp)
|
||||
}
|
||||
|
||||
// Forward a response
|
||||
func ForwardResponse(w http.ResponseWriter, resp *http.Response) {
|
||||
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
w.Write(body)
|
||||
}
|
||||
76
pkg/utils/json_resp_test.go
Normal file
76
pkg/utils/json_resp_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
m "fiskerinc.com/modules/common"
|
||||
th "fiskerinc.com/modules/testhelper"
|
||||
u "fiskerinc.com/modules/utils"
|
||||
)
|
||||
|
||||
func TestRespError(t *testing.T) {
|
||||
errMessage := "TEST_ERROR"
|
||||
response := httptest.NewRecorder()
|
||||
result := m.JSONError{}
|
||||
|
||||
u.RespError(response, http.StatusBadRequest, errMessage)
|
||||
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if response.Result().StatusCode != http.StatusBadRequest {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespError", http.StatusBadRequest, response.Result().StatusCode)
|
||||
}
|
||||
|
||||
if result.Error != http.StatusText(http.StatusBadRequest) {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespError", http.StatusText(http.StatusBadRequest), result.Error)
|
||||
}
|
||||
|
||||
if result.Message != errMessage {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespError", errMessage, result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRespLink(t *testing.T) {
|
||||
linkResult := "TEST_LINK"
|
||||
response := httptest.NewRecorder()
|
||||
result := m.JSONLink{}
|
||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
|
||||
u.RespLink(response, linkResult, timestamp)
|
||||
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if response.Result().StatusCode != http.StatusOK {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespLink", http.StatusOK, response.Result().StatusCode)
|
||||
}
|
||||
|
||||
if result.Link == "" {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespLink", "not empty", result.Link)
|
||||
}
|
||||
|
||||
if result.Link != linkResult {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespLink", linkResult, result.Link)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkJSON(t *testing.T) {
|
||||
link := "TEST_LINK"
|
||||
timestamp := time.Now().Unix()
|
||||
result := u.LinkResult(link, timestamp)
|
||||
|
||||
if result.Link == "" {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespLink", "not empty", result.Link)
|
||||
}
|
||||
|
||||
if result.Link != link {
|
||||
t.Errorf(th.TestErrorTemplate, "TestRespLink", link, result.Link)
|
||||
}
|
||||
}
|
||||
55
pkg/utils/json_rpc_resp.go
Normal file
55
pkg/utils/json_rpc_resp.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
)
|
||||
|
||||
const ErrCodeJSONRPCParse = -32700
|
||||
const ErrCodeJSONRPCInvalidRequest = -32600
|
||||
const ErrCodeJSONRPCMethodNotFound = -32601
|
||||
const ErrCodeJSONRPCInvalidParams = -32602
|
||||
const ErrCodeJSONRPCInternalError = -32603
|
||||
const ErrCodeJSONRPCServerError = -32000
|
||||
|
||||
var ErrJSONRPCParse = errors.New("Parse error")
|
||||
var ErrJSONRPCInvalidRequest = errors.New("Invalid Request")
|
||||
var ErrJSONRPCMethodNotFound = errors.New("Method not found")
|
||||
var ErrJSONRPCInvalidParams = errors.New("Invalid params")
|
||||
var ErrJSONRPCInternalError = errors.New("Internal error")
|
||||
var ErrJSONRPCServerError = errors.New("Server error")
|
||||
|
||||
var ErrCodeJSONRPC map[error]int = map[error]int{
|
||||
ErrJSONRPCParse: ErrCodeJSONRPCParse,
|
||||
ErrJSONRPCInvalidRequest: ErrCodeJSONRPCInvalidRequest,
|
||||
ErrJSONRPCMethodNotFound: ErrCodeJSONRPCMethodNotFound,
|
||||
ErrJSONRPCInvalidParams: ErrCodeJSONRPCInvalidParams,
|
||||
ErrJSONRPCInternalError: ErrCodeJSONRPCInternalError,
|
||||
ErrJSONRPCServerError: ErrCodeJSONRPCServerError,
|
||||
}
|
||||
|
||||
var StatsCodeJSONRPC map[error]int = map[error]int{
|
||||
ErrJSONRPCParse: http.StatusBadRequest,
|
||||
ErrJSONRPCInvalidRequest: http.StatusBadRequest,
|
||||
ErrJSONRPCMethodNotFound: http.StatusNotFound,
|
||||
ErrJSONRPCInvalidParams: http.StatusBadRequest,
|
||||
ErrJSONRPCInternalError: http.StatusInternalServerError,
|
||||
ErrJSONRPCServerError: http.StatusServiceUnavailable,
|
||||
}
|
||||
|
||||
func RespJSONRPCError(w http.ResponseWriter, err error, msg string) {
|
||||
code, ok := ErrCodeJSONRPC[err]
|
||||
if !ok {
|
||||
code = ErrCodeJSONRPCInternalError
|
||||
}
|
||||
resp := common.NewJSONRPCErrorResponse(code, fmt.Sprintf("%v. %s", err.Error(), msg))
|
||||
status, ok := StatsCodeJSONRPC[err]
|
||||
if !ok {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
RespJSON(w, status, resp)
|
||||
}
|
||||
41
pkg/utils/map_helper.go
Normal file
41
pkg/utils/map_helper.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
type MapHelper struct {
|
||||
}
|
||||
|
||||
func (h *MapHelper) GetString(m map[string]interface{}, key string, defaultVal string) string {
|
||||
val, ok := m[key]
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
result, ok := val.(string)
|
||||
if !ok {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *MapHelper) GetData(m map[string]interface{}, key string) []byte {
|
||||
val, ok := m[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO maybe there is a better way to serialize and deserialize JSON RPC binary data
|
||||
// Go by default does not convert []uint JSON into []byte. Thus the code below
|
||||
vals, ok := val.([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var x int64
|
||||
result := make([]byte, len(vals))
|
||||
for i, value := range vals {
|
||||
x = int64(value.(float64))
|
||||
result[i] = byte(x)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
30
pkg/utils/mt19937/bernoulli_distribution.go
Normal file
30
pkg/utils/mt19937/bernoulli_distribution.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package mt19937
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/logger"
|
||||
)
|
||||
|
||||
type bernoulli_distribution struct {
|
||||
m_prob float64
|
||||
m_eng *MT19937
|
||||
}
|
||||
|
||||
func DistBernolli(eng *MT19937, prob float64) *bernoulli_distribution {
|
||||
if prob < 0 || prob > 1 {
|
||||
logger.Error().Msg("prob must be between 0 and 1")
|
||||
return nil
|
||||
}
|
||||
dist := &bernoulli_distribution{
|
||||
m_prob: prob,
|
||||
m_eng: eng,
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func (dist *bernoulli_distribution) Bool() bool {
|
||||
if dist.m_prob == 0 {
|
||||
return false
|
||||
} else {
|
||||
return float64(dist.m_eng.Random()) <= dist.m_prob*float64(^uint64(0))
|
||||
}
|
||||
}
|
||||
81
pkg/utils/mt19937/discrete_distribution.go
Normal file
81
pkg/utils/mt19937/discrete_distribution.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package mt19937
|
||||
|
||||
import "fiskerinc.com/modules/logger"
|
||||
|
||||
type aliasTableUnit struct {
|
||||
weight float64
|
||||
index int
|
||||
}
|
||||
|
||||
type aliasTable struct {
|
||||
m_aliasTable []aliasTableUnit
|
||||
m_eng *MT19937
|
||||
}
|
||||
|
||||
func DistDiscrete(eng *MT19937, list []int) *aliasTable {
|
||||
if len(list) == 0 {
|
||||
logger.Error().Msg("list is not allowed empty!")
|
||||
return nil
|
||||
}
|
||||
var weightSum int
|
||||
for _, weight := range list {
|
||||
weightSum += weight
|
||||
}
|
||||
average := float64(weightSum) / float64(len(list))
|
||||
|
||||
table := &aliasTable{
|
||||
m_aliasTable: make([]aliasTableUnit, len(list)),
|
||||
m_eng: eng,
|
||||
}
|
||||
|
||||
below_average := make([]aliasTableUnit, 0, len(list))
|
||||
above_average := make([]aliasTableUnit, 0, len(list))
|
||||
|
||||
for i, weight := range list {
|
||||
val := float64(weight) / average
|
||||
unit := aliasTableUnit{
|
||||
weight: val,
|
||||
index: i,
|
||||
}
|
||||
if val < 1 {
|
||||
below_average = append(below_average, unit)
|
||||
} else {
|
||||
above_average = append(above_average, unit)
|
||||
}
|
||||
}
|
||||
|
||||
posA := 0
|
||||
posB := 0
|
||||
for posB < len(below_average) && posA < len(above_average) {
|
||||
table.m_aliasTable[below_average[posB].index] = aliasTableUnit{
|
||||
weight: below_average[posB].weight,
|
||||
index: above_average[posA].index,
|
||||
}
|
||||
above_average[posA].weight -= (1 - below_average[posB].weight)
|
||||
if above_average[posA].weight < 1 {
|
||||
below_average[posB] = above_average[posA]
|
||||
posA++
|
||||
} else {
|
||||
posB++
|
||||
}
|
||||
}
|
||||
|
||||
for ; posB < len(below_average); posB++ {
|
||||
table.m_aliasTable[below_average[posB].index].weight = float64(1)
|
||||
}
|
||||
for ; posA < len(above_average); posA++ {
|
||||
table.m_aliasTable[above_average[posA].index].weight = float64(1)
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
func (dist *aliasTable) Discrete() int {
|
||||
result := int(DistInt64(dist.m_eng, 0, int64(len(dist.m_aliasTable)-1)).Int64())
|
||||
test := Dist01(dist.m_eng).Float64()
|
||||
if test < dist.m_aliasTable[result].weight {
|
||||
return result
|
||||
} else {
|
||||
return dist.m_aliasTable[result].index
|
||||
}
|
||||
}
|
||||
92
pkg/utils/mt19937/mt19937.go
Normal file
92
pkg/utils/mt19937/mt19937.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package mt19937
|
||||
|
||||
type MT19937 struct {
|
||||
state []uint64
|
||||
index int
|
||||
}
|
||||
|
||||
func New() *MT19937 {
|
||||
mt := &MT19937{
|
||||
state: make([]uint64, n),
|
||||
index: n,
|
||||
}
|
||||
mt.Seed(5489)
|
||||
return mt
|
||||
}
|
||||
|
||||
func (mt *MT19937) Seed(seed uint64) {
|
||||
x := mt.state
|
||||
x[0] = seed
|
||||
for i := 1; i < n; i++ {
|
||||
x[i] = f*(x[i-1]^(x[i-1]>>(w-2))) + uint64(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (mt *MT19937) Random() uint64 {
|
||||
x := mt.state
|
||||
if mt.index == n {
|
||||
mt.twist()
|
||||
}
|
||||
var z uint64 = x[mt.index]
|
||||
|
||||
mt.index++
|
||||
|
||||
z ^= ((z >> u) & d)
|
||||
z ^= ((z << s) & b)
|
||||
z ^= ((z << t) & c)
|
||||
z ^= (z >> l)
|
||||
return z
|
||||
}
|
||||
|
||||
const (
|
||||
// The variables used in the algorithm are as follows:
|
||||
// w: length (in bits)
|
||||
// n: recursion length
|
||||
// m: period parameter, used as the offset of the third stage
|
||||
// r: low-order mask / low-order bits to be extracted
|
||||
// a: the parameters of the rotation matrix
|
||||
// b,c: TGFSR mask
|
||||
// s, t: the displacement of TGFSR
|
||||
// u,d,l: mask and displacement required for additional mason rotation
|
||||
// f: Initialize the required parameters of the Mason rotating chain
|
||||
|
||||
w = 64
|
||||
n = 312
|
||||
m = 156
|
||||
r = 31
|
||||
|
||||
a = 0xb5026f5aa96619e9
|
||||
|
||||
u = 29
|
||||
d = 0x5555555555555555
|
||||
|
||||
s = 17
|
||||
b = 0x71d67fffeda60000
|
||||
|
||||
t = 37
|
||||
c = 0xfff7eee000000000
|
||||
|
||||
l = 43
|
||||
|
||||
f = 6364136223846793005
|
||||
)
|
||||
|
||||
func (mt *MT19937) twist() {
|
||||
x := mt.state
|
||||
const lower_mask uint64 = 1<<r - 1
|
||||
const upper_mask uint64 = ^lower_mask
|
||||
|
||||
for j := 0; j < n-m; j++ {
|
||||
var y uint64 = (x[j] & upper_mask) | (x[j+1] & lower_mask)
|
||||
x[j] = x[j+m] ^ (y >> 1) ^ ((x[j+1] & 1) * a)
|
||||
}
|
||||
|
||||
for j := n - m; j < n-1; j++ {
|
||||
var y uint64 = (x[j] & upper_mask) | (x[j+1] & lower_mask)
|
||||
x[j] = x[j-(n-m)] ^ (y >> 1) ^ ((x[j+1] & 1) * a)
|
||||
}
|
||||
|
||||
var y uint64 = (x[n-1] & upper_mask) | (x[0] & lower_mask)
|
||||
x[n-1] = x[m-1] ^ (y >> 1) ^ ((x[0] & 1) * a)
|
||||
mt.index = 0
|
||||
}
|
||||
16
pkg/utils/mt19937/uniform_01.go
Normal file
16
pkg/utils/mt19937/uniform_01.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package mt19937
|
||||
|
||||
type uniform_01 struct {
|
||||
m_eng *MT19937
|
||||
}
|
||||
|
||||
func Dist01(eng *MT19937) *uniform_01 {
|
||||
dist := &uniform_01{
|
||||
m_eng: eng,
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func (dist *uniform_01) Float64() float64 {
|
||||
return float64(dist.m_eng.Random()) / float64(^uint64(0))
|
||||
}
|
||||
49
pkg/utils/mt19937/uniform_int_distribution.go
Normal file
49
pkg/utils/mt19937/uniform_int_distribution.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package mt19937
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
type UniformIntDistribution struct {
|
||||
m_min int64
|
||||
m_max int64
|
||||
m_eng *MT19937
|
||||
}
|
||||
|
||||
func DistInt64(eng *MT19937, begin int64, end int64) *UniformIntDistribution {
|
||||
if begin > end {
|
||||
log.Println("ERROR! begin is not allowed to be greater than end!")
|
||||
return nil
|
||||
}
|
||||
|
||||
dist := &UniformIntDistribution{
|
||||
m_min: begin,
|
||||
m_max: end,
|
||||
m_eng: eng,
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func (dist *UniformIntDistribution) Int64() int64 {
|
||||
var rng uint64
|
||||
|
||||
if dist.m_min >= 0 {
|
||||
rng = uint64(dist.m_max) - uint64(dist.m_min)
|
||||
} else if dist.m_max >= 0 {
|
||||
rng = uint64(dist.m_max) + uint64(-(dist.m_min + 1)) + 1
|
||||
} else {
|
||||
rng = uint64(dist.m_max - dist.m_min)
|
||||
}
|
||||
|
||||
if rng == 0 {
|
||||
return dist.m_min
|
||||
} else if rng == ^uint64(0) {
|
||||
return int64(dist.m_eng.Random())
|
||||
}
|
||||
bucket_size := ^uint64(0) / (rng + 1)
|
||||
if ^uint64(0)%(rng+1) == rng {
|
||||
bucket_size++
|
||||
}
|
||||
result := dist.m_eng.Random() / bucket_size
|
||||
return int64(result) + dist.m_min
|
||||
}
|
||||
159
pkg/utils/multipart_parser.go
Normal file
159
pkg/utils/multipart_parser.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const chunkSize int = 4096
|
||||
|
||||
// FileInfo used to send part filename and content type
|
||||
type FileInfo struct {
|
||||
FileID string
|
||||
Filename string
|
||||
ContentType string
|
||||
Part *multipart.Part
|
||||
FileSize uint64
|
||||
OrigFileSize uint64
|
||||
}
|
||||
|
||||
// FormParser type func that handles parsing of form fields values
|
||||
type FormParser func(part *multipart.Part) error
|
||||
type FileEncryptor func(input []byte) []byte
|
||||
|
||||
// FindFilePart finds file part of multipart upload
|
||||
func FindFilePart(r *http.Request, boundary string, fileFieldName string, formParser FormParser) (*FileInfo, error) {
|
||||
result := &FileInfo{}
|
||||
mr := multipart.NewReader(r.Body, boundary)
|
||||
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return result, nil
|
||||
} else if err != nil {
|
||||
return result, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if part.FormName() != fileFieldName || len(part.FileName()) == 0 {
|
||||
if formParser != nil {
|
||||
err = formParser(part)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
part.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
result.Filename = part.FileName()
|
||||
result.ContentType = GetHTTPHeader(part.Header, "Content-Type", "")
|
||||
len, _ := strconv.ParseInt(GetHTTPHeader(part.Header, "Content-Length", "0"), 10, 32)
|
||||
result.OrigFileSize = uint64(len)
|
||||
result.Part = part
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkFilePart chunks file part into pipe writer
|
||||
func ChunkFilePart(writer *io.PipeWriter, info *FileInfo, encryptor FileEncryptor) {
|
||||
var err error
|
||||
var origFileSize uint64
|
||||
var fileSize uint64
|
||||
defer writer.Close()
|
||||
|
||||
n := 0
|
||||
buf := make([]byte, chunkSize)
|
||||
|
||||
for {
|
||||
n, err = io.ReadFull(info.Part, buf)
|
||||
if n > 0 {
|
||||
if encryptor != nil {
|
||||
out := encryptor(buf[:n])
|
||||
writer.Write(out)
|
||||
fileSize += uint64(len(out))
|
||||
} else {
|
||||
writer.Write(buf[:n])
|
||||
fileSize += uint64(n)
|
||||
}
|
||||
origFileSize += uint64(n)
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
if info != nil {
|
||||
info.FileSize = fileSize
|
||||
info.OrigFileSize = origFileSize
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PartReadAll returns entire part value. Maxchars checks that max characters is not exceeded
|
||||
func PartReadAll(part *multipart.Part, maxchars int) (string, error) {
|
||||
var err error
|
||||
n := 0
|
||||
buf := make([]byte, chunkSize)
|
||||
result := []byte{}
|
||||
|
||||
for {
|
||||
n, err = part.Read(buf)
|
||||
result = append(result, buf[:n]...)
|
||||
if len(result) > maxchars {
|
||||
return "", errors.Errorf("%s exceeded %d characters", part.FormName(), maxchars)
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
return string(result), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HashMultipartFile(file multipart.File) ([]byte, error) {
|
||||
var err error
|
||||
n := 0
|
||||
buf := make([]byte, chunkSize)
|
||||
digest := sha256.New()
|
||||
|
||||
for {
|
||||
n, err = file.Read(buf)
|
||||
if n > 0 {
|
||||
_, err := digest.Write(buf[:n])
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
}
|
||||
sum := digest.Sum(nil)
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func PartReadInt64(part *multipart.Part, maxchars int, bitSize int) (int64, error) {
|
||||
value, err := PartReadAll(part, maxchars)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
return strconv.ParseInt(value, 10, bitSize)
|
||||
}
|
||||
|
||||
func PartReadUInt64(part *multipart.Part, maxchars int, bitSize int) (uint64, error) {
|
||||
value, err := PartReadAll(part, maxchars)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
return strconv.ParseUint(value, 10, bitSize)
|
||||
}
|
||||
|
||||
func PartReadInt(part *multipart.Part, maxchars int) (int, error) {
|
||||
value, err := PartReadInt64(part, maxchars, 32)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
return int(value), nil
|
||||
}
|
||||
171
pkg/utils/multipart_parser_test.go
Normal file
171
pkg/utils/multipart_parser_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"fiskerinc.com/modules/utils"
|
||||
)
|
||||
|
||||
func TestMultipartParser(t *testing.T) {
|
||||
err := execMultipartParser()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMutipartParser(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
execMultipartParser()
|
||||
}
|
||||
}
|
||||
|
||||
func execMultipartParser() error {
|
||||
|
||||
r := getRequestPOSTFormFile()
|
||||
|
||||
mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
return errors.New("requires multipart")
|
||||
}
|
||||
|
||||
if len(params["boundary"]) > 70 {
|
||||
return errors.New("boundary too long")
|
||||
}
|
||||
|
||||
info, err := utils.FindFilePart(r, params["boundary"], "file", nil)
|
||||
if info.Part == nil {
|
||||
return errors.New("file not found")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encryptor := func(input []byte) []byte {
|
||||
return input
|
||||
}
|
||||
pReader, pWriter := io.Pipe()
|
||||
defer pReader.Close()
|
||||
defer info.Part.Close()
|
||||
|
||||
go utils.ChunkFilePart(pWriter, info, encryptor)
|
||||
err = func() error {
|
||||
_, err := ioutil.ReadAll(pReader)
|
||||
return err
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRequestPOSTFormFile() *http.Request {
|
||||
postData :=
|
||||
`--xxx
|
||||
Content-Disposition: form-data; name="field1"
|
||||
|
||||
value1
|
||||
--xxx
|
||||
Content-Disposition: form-data; name="file"; filename="test.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
We're leavin' together
|
||||
But still it's farewell
|
||||
And maybe we'll come back
|
||||
To Earth, who can tell?
|
||||
I guess there is no one to blame
|
||||
We're leaving ground (leaving ground)
|
||||
Will things ever be the same again?
|
||||
It's the final countdown
|
||||
The final countdown
|
||||
|
||||
--xxx--
|
||||
`
|
||||
request, err := http.NewRequest(http.MethodPost, "/", ioutil.NopCloser(strings.NewReader(postData)))
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
}
|
||||
request.Header.Add("Content-Type", "multipart/form-data; boundary=xxx")
|
||||
return request
|
||||
}
|
||||
278
pkg/utils/quadkey/quadkey.go
Normal file
278
pkg/utils/quadkey/quadkey.go
Normal file
@@ -0,0 +1,278 @@
|
||||
package quadkey
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_LATITUDE = 85.05112877980659
|
||||
MAX_LONGITUDE = 180.0
|
||||
zoom = 32
|
||||
EARTH_RADIUS = 6378137.0 //meters
|
||||
EARTH_CIRCUMFERENCE = 2.0 * math.Pi * EARTH_RADIUS
|
||||
METERS_TO_XY = 1.0 / EARTH_CIRCUMFERENCE * ((1 << zoom) - 1) //conversion coefficient between meters to XY
|
||||
XY_TO_METERS = EARTH_CIRCUMFERENCE / ((1 << zoom) - 1) //conversion coefficient from XY to meters
|
||||
)
|
||||
|
||||
// Returns integer (X, Y) in range 0 - (1<<32) where 1<<32 is mapped to MAX LATITUDE, MAX LONGITUDE
|
||||
// XY (0,0) corresponds to LatLong (90, -180)
|
||||
func LatLongToXY(lat float64, long float64) (uint64, uint64) {
|
||||
lat = math.Min(MAX_LATITUDE, math.Max(-MAX_LATITUDE, lat))
|
||||
long = math.Min(MAX_LONGITUDE, math.Max(-MAX_LONGITUDE, long))
|
||||
|
||||
//fx and fy compute fractional x, y in range 0.0-1.0
|
||||
fx := long/360.0 + 0.5
|
||||
sinlat := math.Sin(lat * math.Pi / 180.0)
|
||||
fy := 0.5 - math.Log((1+sinlat)/(1-sinlat))/(4*math.Pi)
|
||||
|
||||
scale := 1 << zoom
|
||||
x := math.Min(float64(scale-1), math.Max(0, math.Floor(fx*float64(scale))))
|
||||
y := math.Min(float64(scale-1), math.Max(0, math.Floor(fy*float64(scale))))
|
||||
return uint64(x), uint64(y)
|
||||
}
|
||||
|
||||
// converts XY to quadkey, where XY is int in range 0 - (1<<32).
|
||||
// taken from example code https://github.com/CartoDB/python-quadkey/
|
||||
// this implementation only works with ZOOM=32
|
||||
func XYToQuadkey(x uint64, y uint64) uint64 {
|
||||
B := []uint64{0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F, 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF}
|
||||
S := []uint64{1, 2, 4, 8, 16}
|
||||
|
||||
x = (x | (x << S[4])) & B[4]
|
||||
y = (y | (y << S[4])) & B[4]
|
||||
|
||||
x = (x | (x << S[3])) & B[3]
|
||||
y = (y | (y << S[3])) & B[3]
|
||||
|
||||
x = (x | (x << S[2])) & B[2]
|
||||
y = (y | (y << S[2])) & B[2]
|
||||
|
||||
x = (x | (x << S[1])) & B[1]
|
||||
y = (y | (y << S[1])) & B[1]
|
||||
|
||||
x = (x | (x << S[0])) & B[0]
|
||||
y = (y | (y << S[0])) & B[0]
|
||||
|
||||
return x | (y << 1)
|
||||
}
|
||||
|
||||
func LatLongToQuadKey(lat float64, long float64) uint64 {
|
||||
x, y := LatLongToXY(lat, long)
|
||||
return XYToQuadkey(x, y)
|
||||
}
|
||||
|
||||
// converts quadkey back to XY, where XY is int in range 0 - (1<<32).
|
||||
// taken from example code https://github.com/CartoDB/python-quadkey/
|
||||
func QuadkeyToXY(quadkey uint64) (uint64, uint64) {
|
||||
B := []uint64{0x5555555555555555, 0x3333333333333333, 0x0F0F0F0F0F0F0F0F, 0x00FF00FF00FF00FF, 0x0000FFFF0000FFFF, 0x00000000FFFFFFFF}
|
||||
S := []uint64{0, 1, 2, 4, 8, 16}
|
||||
x := quadkey
|
||||
y := quadkey >> 1
|
||||
|
||||
x = (x | (x >> S[0])) & B[0]
|
||||
y = (y | (y >> S[0])) & B[0]
|
||||
|
||||
x = (x | (x >> S[1])) & B[1]
|
||||
y = (y | (y >> S[1])) & B[1]
|
||||
|
||||
x = (x | (x >> S[2])) & B[2]
|
||||
y = (y | (y >> S[2])) & B[2]
|
||||
|
||||
x = (x | (x >> S[3])) & B[3]
|
||||
y = (y | (y >> S[3])) & B[3]
|
||||
|
||||
x = (x | (x >> S[4])) & B[4]
|
||||
y = (y | (y >> S[4])) & B[4]
|
||||
|
||||
x = (x | (x >> S[5])) & B[5]
|
||||
y = (y | (y >> S[5])) & B[5]
|
||||
return x, y
|
||||
}
|
||||
|
||||
// converts a uint64 quadkey to a string representation.
|
||||
// the string can be visualized using https://labs.mapbox.com/what-the-tile/
|
||||
func QuadkeyStr(quadkey uint64) string {
|
||||
sb := strings.Builder{}
|
||||
sb.Grow(zoom)
|
||||
|
||||
for offset := 62; offset >= 0; offset -= 2 {
|
||||
digit := (quadkey >> offset) & 3
|
||||
switch digit {
|
||||
case 0:
|
||||
sb.WriteByte('0')
|
||||
case 1:
|
||||
sb.WriteByte('1')
|
||||
case 2:
|
||||
sb.WriteByte('2')
|
||||
case 3:
|
||||
sb.WriteByte('3')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// returns a list of quadkey buckets that can be used with a SQL query.
|
||||
// this performs a dfs with depth limited by coarseness; so small coarseness can be slow.
|
||||
// this is a much more accurate search for buckets in rect
|
||||
//
|
||||
// radius is in meters
|
||||
// coarseness is in meters, and defines the depth of buckets
|
||||
func QuadkeySearchBuckets(lat float64, long float64, radius float64, coarseness float64) map[uint64]QuadkeyBucket {
|
||||
maxDepth := coarseToDepth(coarseness)
|
||||
x, y := LatLongToXY(lat, long)
|
||||
radiusXY := uint64(radius * METERS_TO_XY)
|
||||
|
||||
searchQuad := quad{
|
||||
x - radiusXY,
|
||||
x + radiusXY,
|
||||
y - radiusXY,
|
||||
y + radiusXY,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
|
||||
bucketsMap := map[uint64]QuadkeyBucket{}
|
||||
|
||||
rectBucketSearch(&searchQuad, 0x0000000000000000, 0, maxDepth, bucketsMap)
|
||||
rectBucketSearch(&searchQuad, 0x4000000000000000, 0, maxDepth, bucketsMap)
|
||||
rectBucketSearch(&searchQuad, 0x8000000000000000, 0, maxDepth, bucketsMap)
|
||||
rectBucketSearch(&searchQuad, 0xC000000000000000, 0, maxDepth, bucketsMap)
|
||||
|
||||
return bucketsMap
|
||||
}
|
||||
|
||||
// returns a list of quadkey buckets that can be used with a SQL query.
|
||||
// this does grid lookup for quadkeys, using coarseness as a stepsize.
|
||||
// this is very inaccurate when coarseness is larger than radius
|
||||
//
|
||||
// radius is in meters
|
||||
// coarseness is in meters, and defines the depth of buckets and stepsize for grid.
|
||||
// coarseness is clamped to 2x radius
|
||||
func QuadkeyGridBuckets(lat float64, long float64, radius float64, coarseness float64) map[uint64]QuadkeyBucket {
|
||||
depth := coarseToDepth(coarseness)
|
||||
x, y := LatLongToXY(lat, long)
|
||||
radiusXY := uint64(radius * METERS_TO_XY)
|
||||
stepXY := uint64(math.Min(coarseness, 2*radius) * METERS_TO_XY)
|
||||
|
||||
var mask uint64 = 0xFFFFFFFFFFFFFFFF << (64 - depth*2 - 2)
|
||||
|
||||
x -= radiusXY
|
||||
y -= radiusXY
|
||||
|
||||
bucketsMap := map[uint64]QuadkeyBucket{}
|
||||
|
||||
for i := uint64(0); i <= 2*radiusXY; i += stepXY {
|
||||
for j := uint64(0); j <= 2*radiusXY; j += stepXY {
|
||||
qkey := XYToQuadkey(x+i, y+j)
|
||||
qkey &= mask
|
||||
|
||||
_, ok := bucketsMap[qkey]
|
||||
if !ok {
|
||||
bucketsMap[qkey] = NewQuadkeyBucket(qkey, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bucketsMap
|
||||
}
|
||||
|
||||
// recursive dfs search down to desired maxdepth.
|
||||
// quadkeys are appended to buckets map
|
||||
func rectBucketSearch(searchQuad *quad, node uint64, depth int, maxDepth int, buckets map[uint64]QuadkeyBucket) {
|
||||
nodeQuad := QuadkeyToQuad(node, depth)
|
||||
|
||||
if !nodeQuad.Intersects(searchQuad) {
|
||||
return
|
||||
}
|
||||
|
||||
if depth < maxDepth {
|
||||
offset := 62 - (depth*2 + 2)
|
||||
rectBucketSearch(searchQuad, node, depth+1, maxDepth, buckets) //child 0
|
||||
rectBucketSearch(searchQuad, node|(1<<offset), depth+1, maxDepth, buckets) //child 1
|
||||
rectBucketSearch(searchQuad, node|(2<<offset), depth+1, maxDepth, buckets) //child 2
|
||||
rectBucketSearch(searchQuad, node|(3<<offset), depth+1, maxDepth, buckets) //child 3
|
||||
} else {
|
||||
_, ok := buckets[node]
|
||||
if !ok {
|
||||
buckets[node] = NewQuadkeyBucket(node, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// converts a coarseness (meters) to a quadtree depth (int)
|
||||
// depth 0 at maximum coarseness is bit 63. depth 32 at max resolution is bit 0
|
||||
func coarseToDepth(coarseness float64) int {
|
||||
size := EARTH_CIRCUMFERENCE / 2.0
|
||||
depth := 0
|
||||
for size > coarseness {
|
||||
depth++
|
||||
size /= 2.0
|
||||
}
|
||||
return depth - 1
|
||||
}
|
||||
|
||||
// a quad is a rectangle used for intersection checks when performing
|
||||
// quadtree searches
|
||||
type quad struct {
|
||||
x0 uint64
|
||||
x1 uint64
|
||||
y0 uint64
|
||||
y1 uint64
|
||||
|
||||
Quadkey uint64
|
||||
Depth int
|
||||
}
|
||||
|
||||
func (q *quad) Intersects(other *quad) bool {
|
||||
if q.x0 >= q.x1 || q.y0 >= q.y1 || other.x0 >= other.x1 || other.y0 >= other.y1 {
|
||||
return false
|
||||
}
|
||||
if q.x0 > other.x1 || other.x0 > q.x1 {
|
||||
return false
|
||||
}
|
||||
if q.y0 > other.y1 || other.y0 > q.y1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// creates a quad from the given quadkey and depth
|
||||
func QuadkeyToQuad(quadkey uint64, depth int) quad {
|
||||
var mask uint64 = 0xFFFFFFFFFFFFFFFF << (64 - depth*2 - 2)
|
||||
quadkey &= mask
|
||||
width := uint64(1<<(32-depth-1) - 1)
|
||||
|
||||
x0, y0 := QuadkeyToXY(quadkey)
|
||||
|
||||
q := quad{
|
||||
x0: x0,
|
||||
y0: y0,
|
||||
x1: x0 + width,
|
||||
y1: y0 + width,
|
||||
Quadkey: quadkey,
|
||||
Depth: depth,
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// A QuadkeyBucket represents a contiguous range of quadkey indices that can be searched in a SQL db.
|
||||
// an example query might use "WHERE quadkey BETWEEN {bucket.Start} AND {bucket.End}"
|
||||
type QuadkeyBucket struct {
|
||||
Quadkey uint64 //the parent node for the bucket
|
||||
Depth int //parent node depth for the bucket
|
||||
Start uint64 //Start Index of the bucket
|
||||
End uint64 //End Index of the bucket
|
||||
}
|
||||
|
||||
func NewQuadkeyBucket(quadkey uint64, depth int) QuadkeyBucket {
|
||||
var mask uint64 = 0xFFFFFFFFFFFFFFFF << (64 - depth*2 - 2)
|
||||
|
||||
return QuadkeyBucket{
|
||||
quadkey,
|
||||
depth,
|
||||
quadkey & mask,
|
||||
quadkey | (^mask),
|
||||
}
|
||||
}
|
||||
460
pkg/utils/quadkey/quadkey_test.go
Normal file
460
pkg/utils/quadkey/quadkey_test.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package quadkey_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/utils/quadkey"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLatLongToQuadkey(t *testing.T) {
|
||||
|
||||
tests := map[string]struct {
|
||||
lat float64
|
||||
long float64
|
||||
expectedQuadkeyStr string
|
||||
expectedQuadkeyInt uint64
|
||||
}{
|
||||
"lapalma": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
expectedQuadkeyStr: "02301320022100032301331202320110",
|
||||
expectedQuadkeyInt: 3204356230721449492,
|
||||
},
|
||||
|
||||
"london": {
|
||||
lat: 51.507822,
|
||||
long: -0.162069,
|
||||
expectedQuadkeyStr: "03131313113000100311131122203023",
|
||||
expectedQuadkeyInt: 3996764367461132491,
|
||||
},
|
||||
|
||||
"null": {
|
||||
lat: 0.0,
|
||||
long: 0.0,
|
||||
expectedQuadkeyStr: "30000000000000000000000000000000",
|
||||
expectedQuadkeyInt: 0xC000000000000000,
|
||||
},
|
||||
|
||||
"invalid": {
|
||||
lat: -2000.0,
|
||||
long: -2000.0,
|
||||
expectedQuadkeyStr: "22222222222222222222222222222222",
|
||||
expectedQuadkeyInt: 0xAAAAAAAAAAAAAAAA,
|
||||
},
|
||||
}
|
||||
|
||||
for tname, tt := range tests {
|
||||
t.Run(tname, func(t *testing.T) {
|
||||
|
||||
qkey := quadkey.LatLongToQuadKey(tt.lat, tt.long)
|
||||
qkeyStr := quadkey.QuadkeyStr(qkey)
|
||||
|
||||
assert.Equal(t, tt.expectedQuadkeyInt, qkey)
|
||||
assert.Equal(t, tt.expectedQuadkeyStr, qkeyStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuadkeySearchBuckets(t *testing.T) {
|
||||
|
||||
tests := map[string]struct {
|
||||
lat float64
|
||||
long float64
|
||||
radius float64
|
||||
coarseness float64
|
||||
|
||||
expectedKeys map[uint64]bool
|
||||
expectedBuckets map[uint64]quadkey.QuadkeyBucket
|
||||
absentKeys map[uint64]bool //set of keys that should not show up
|
||||
}{
|
||||
|
||||
"invalid": {
|
||||
lat: -2000.0,
|
||||
long: -2000.0,
|
||||
radius: 100.0, //100m
|
||||
coarseness: 600000.0, // 600km
|
||||
expectedKeys: map[uint64]bool{}, //an invalid quad is generated, and fails every intersect check
|
||||
},
|
||||
|
||||
"null": {
|
||||
lat: 0.0,
|
||||
long: 0.0,
|
||||
radius: 100.0, //100m
|
||||
coarseness: 600000.0, // 600km
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x3FF0000000000000: true, //033333
|
||||
0xC000000000000000: true, //300000
|
||||
0x6AA0000000000000: true, //122222
|
||||
0x9550000000000000: true, //211111
|
||||
},
|
||||
},
|
||||
|
||||
"london": {
|
||||
lat: 51.507822,
|
||||
long: -0.162069,
|
||||
radius: 8, //8 meters radius
|
||||
coarseness: 8,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x37775C0435400000: true, //0313131311300010031110
|
||||
0x37775C0435500000: true, //0313131311300010031111
|
||||
0x37775C0460000000: true, //0313131311300010120000
|
||||
0x37775C0435600000: true, //0313131311300010031112
|
||||
0x37775C0435700000: true, //0313131311300010031113
|
||||
0x37775C0460200000: true, //0313131311300010120002
|
||||
0x37775C0435C00000: true, //0313131311300010031130
|
||||
0x37775C0435D00000: true, //0313131311300010031131
|
||||
0x37775C0460800000: true, //0313131311300010120020
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B1DC8000: true,
|
||||
0x2C782903B4000000: true,
|
||||
0x2C782903B4080000: true,
|
||||
0x2C782903B00C0000: true,
|
||||
0x2C782903B4800000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
//lapalma test cases use quadkeys from visually inspecting mapbox maps
|
||||
"lapalma": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8, //8 meters radius
|
||||
coarseness: 4, //looking for buckets with width of 4 meters
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1D80000: true,
|
||||
0x2C782903B1DC0000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
0x2C782903B4A80000: true,
|
||||
0x2C782903B1EC0000: true,
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B1F40000: true,
|
||||
0x2C782903B3540000: true,
|
||||
0x2C782903B6000000: true,
|
||||
0x2C782903B1CC0000: true,
|
||||
0x2C782903B1E40000: true,
|
||||
0x2C782903B1FC0000: true,
|
||||
0x2C782903B3440000: true,
|
||||
0x2C782903B4880000: true,
|
||||
0x2C782903B1F80000: true,
|
||||
0x2C782903B3500000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B1DC8000: true,
|
||||
0x2C782903B4000000: true,
|
||||
0x2C782903B4080000: true,
|
||||
0x2C782903B00C0000: true,
|
||||
0x2C782903B4800000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_2": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 8,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1C00000: true,
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B4800000: true,
|
||||
|
||||
0x2C782903B1E00000: true,
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
|
||||
0x2C782903B3400000: true,
|
||||
0x2C782903B3500000: true,
|
||||
0x2C782903B6000000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B3440000: true,
|
||||
0x2C782903B4880000: true,
|
||||
0x2C782903B1F80000: true,
|
||||
0x2C78290000000000: true,
|
||||
0x2C782903B3000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_3": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 32,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1000000: true,
|
||||
0x2C782903B4000000: true,
|
||||
0x2C782903B3000000: true,
|
||||
0x2C782903B6000000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B3400000: true,
|
||||
0x2C782903B3500000: true,
|
||||
0x2C782903B0000000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_4": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 128, //looking for 128m width nodes
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B0000000: true, //corresponds to 023013200221000323, ~127m width node
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C78000000000000: true,
|
||||
0x2C782903B4000000: true,
|
||||
0x2C782903B3000000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_5": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 100000, //looking for 100km width nodes
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C78000000000000: true, //corresponds to 02301320, ~130km width node
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B0000000: true,
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
|
||||
expectedBuckets: map[uint64]quadkey.QuadkeyBucket{
|
||||
0x2C78000000000000: {
|
||||
0x2C78000000000000,
|
||||
7,
|
||||
0x2C78000000000000,
|
||||
0x2C78FFFFFFFFFFFF,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tname, tt := range tests {
|
||||
t.Run(tname, func(t *testing.T) {
|
||||
buckets := quadkey.QuadkeySearchBuckets(tt.lat, tt.long, tt.radius, tt.coarseness)
|
||||
|
||||
assert.Equal(t, len(tt.expectedKeys), len(buckets))
|
||||
for key := range buckets {
|
||||
assert.Contains(t, tt.expectedKeys, key)
|
||||
}
|
||||
|
||||
for key, bucket := range tt.expectedBuckets {
|
||||
assert.Contains(t, buckets, key)
|
||||
|
||||
actualBucket, ok := buckets[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, bucket.Quadkey, actualBucket.Quadkey)
|
||||
assert.Equal(t, bucket.Depth, actualBucket.Depth)
|
||||
assert.Equal(t, bucket.Start, actualBucket.Start)
|
||||
assert.Equal(t, bucket.End, actualBucket.End)
|
||||
}
|
||||
|
||||
for key := range tt.absentKeys {
|
||||
assert.NotContains(t, buckets, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuadkeyGridBuckets(t *testing.T) {
|
||||
|
||||
tests := map[string]struct {
|
||||
lat float64
|
||||
long float64
|
||||
radius float64
|
||||
coarseness float64
|
||||
|
||||
expectedKeys map[uint64]bool
|
||||
absentKeys map[uint64]bool //set of keys that should not show up
|
||||
}{
|
||||
|
||||
"invalid": {
|
||||
lat: -2000.0,
|
||||
long: -2000.0,
|
||||
radius: 100.0, //100m
|
||||
coarseness: 600000.0, // 600km
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x0000000000000000: true, //clamped lat long. 90, -180
|
||||
0xAAA0000000000000: true, //clamped lat long -90, -180
|
||||
0xFFF0000000000000: true, //clamped lat long, -90, +180
|
||||
0x5550000000000000: true, //clamped lat long, +90, +180
|
||||
},
|
||||
},
|
||||
"null": {
|
||||
lat: 0.0,
|
||||
long: 0.0,
|
||||
radius: 100.0, //1km
|
||||
coarseness: 600000.0, // 600km
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x3FF0000000000000: true, //033333
|
||||
0xC000000000000000: true, //300000
|
||||
0x6AA0000000000000: true, //122222
|
||||
0x9550000000000000: true, //211111
|
||||
},
|
||||
},
|
||||
"london": {
|
||||
lat: 51.507822,
|
||||
long: -0.162069,
|
||||
radius: 8, //8 meters radius
|
||||
coarseness: 8,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x37775C0435400000: true, //0313131311300010031110
|
||||
0x37775C0435500000: true, //0313131311300010031111
|
||||
0x37775C0460000000: true, //0313131311300010120000
|
||||
0x37775C0435600000: true, //0313131311300010031112
|
||||
0x37775C0435700000: true, //0313131311300010031113
|
||||
0x37775C0460200000: true, //0313131311300010120002
|
||||
0x37775C0435C00000: true, //0313131311300010031130
|
||||
0x37775C0435D00000: true, //0313131311300010031131
|
||||
0x37775C0460800000: true, //0313131311300010120020
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B1DC8000: true,
|
||||
0x2C782903B4000000: true,
|
||||
0x2C782903B4080000: true,
|
||||
0x2C782903B00C0000: true,
|
||||
0x2C782903B4800000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
//lapalma test cases use quadkeys from visually inspecting mapbox maps
|
||||
"lapalma": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 4,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1D80000: true,
|
||||
0x2C782903B1DC0000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
0x2C782903B4A80000: true,
|
||||
0x2C782903B1EC0000: true,
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B1F40000: true,
|
||||
0x2C782903B3540000: true,
|
||||
0x2C782903B6000000: true,
|
||||
0x2C782903B1CC0000: true,
|
||||
0x2C782903B1E40000: true,
|
||||
0x2C782903B1FC0000: true,
|
||||
0x2C782903B3440000: true,
|
||||
0x2C782903B4880000: true,
|
||||
0x2C782903B1F80000: true,
|
||||
0x2C782903B3500000: true,
|
||||
},
|
||||
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B3400000: true,
|
||||
0x2C782903B1000000: true,
|
||||
0x2C782903B0000000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_2": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 8,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1C00000: true,
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B4800000: true,
|
||||
|
||||
0x2C782903B1E00000: true,
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
|
||||
0x2C782903B3400000: true,
|
||||
0x2C782903B3500000: true,
|
||||
0x2C782903B6000000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1000000: true,
|
||||
0x2C782903B0000000: true,
|
||||
0x2C78290000000000: true,
|
||||
0x2c782903B4000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_3": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 32,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B1000000: true,
|
||||
0x2c782903B3000000: true,
|
||||
0x2c782903B4000000: true,
|
||||
0x2c782903B6000000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1F00000: true,
|
||||
0x2C782903B4A00000: true,
|
||||
0x2C782903B0000000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
|
||||
"lapalma_4": {
|
||||
lat: 33.86219399999999,
|
||||
long: -118.029596,
|
||||
radius: 8,
|
||||
coarseness: 128,
|
||||
|
||||
expectedKeys: map[uint64]bool{
|
||||
0x2C782903B0000000: true,
|
||||
},
|
||||
absentKeys: map[uint64]bool{
|
||||
0x2C782903B1000000: true,
|
||||
0x2C782903B1D00000: true,
|
||||
0x2C782903B4800000: true,
|
||||
0x2C782903B1CC0000: true,
|
||||
0x2C782903B1E40000: true,
|
||||
0x2C78290000000000: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tname, tt := range tests {
|
||||
t.Run(tname, func(t *testing.T) {
|
||||
fmt.Println(tname)
|
||||
|
||||
buckets := quadkey.QuadkeyGridBuckets(tt.lat, tt.long, tt.radius, tt.coarseness)
|
||||
|
||||
assert.Equal(t, len(tt.expectedKeys), len(buckets))
|
||||
for key := range buckets {
|
||||
assert.Contains(t, tt.expectedKeys, key)
|
||||
}
|
||||
|
||||
for key := range tt.absentKeys {
|
||||
assert.NotContains(t, buckets, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
pkg/utils/querystring/querystring.go
Normal file
41
pkg/utils/querystring/querystring.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package querystring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SplitIntArray(value string) ([]int64, error) {
|
||||
items := strings.Split(value, ",")
|
||||
result := make([]int64, len(items))
|
||||
|
||||
for i, item := range items {
|
||||
val, err := strconv.ParseInt(item, 10, 64)
|
||||
if err != nil || val < 1 {
|
||||
return result, fmt.Errorf("invalid id %s", item)
|
||||
}
|
||||
result[i] = val
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ConvertStringToInt(input string) (int, error) {
|
||||
// Try converting the string to float
|
||||
floatValue, err := strconv.ParseFloat(input, 64)
|
||||
if err == nil {
|
||||
// Float conversion successful, return the integer part
|
||||
return int(floatValue), nil
|
||||
}
|
||||
|
||||
// Float conversion failed, try converting the string to integer
|
||||
intValue, err := strconv.Atoi(input)
|
||||
if err == nil {
|
||||
// Integer conversion successful, return the integer value
|
||||
return intValue, nil
|
||||
}
|
||||
|
||||
// Conversion failed
|
||||
return 0, err
|
||||
}
|
||||
25
pkg/utils/querystring/querystring_test.go
Normal file
25
pkg/utils/querystring/querystring_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package querystring_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/utils/querystring"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertStringToInt(t *testing.T) {
|
||||
input := "3.14"
|
||||
result, err := querystring.ConvertStringToInt(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, result, 3)
|
||||
|
||||
input = "447"
|
||||
result, err = querystring.ConvertStringToInt(input)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, result, 447)
|
||||
|
||||
input = "abc"
|
||||
result, err = querystring.ConvertStringToInt(input)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, result, 0)
|
||||
}
|
||||
44
pkg/utils/randomvalues/noncryptorandomvalues.go
Normal file
44
pkg/utils/randomvalues/noncryptorandomvalues.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package randomvalues
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NonCryptoGenerator struct {
|
||||
characters string
|
||||
*rand.Rand
|
||||
*sync.Mutex
|
||||
}
|
||||
|
||||
// Only set seed when you need a non-random value
|
||||
func NewNonCryptoGenerator(allowedchars string, seed int64) NonCryptoGenerator {
|
||||
if len(allowedchars) == 0 {
|
||||
allowedchars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
}
|
||||
instance := NonCryptoGenerator{
|
||||
characters: allowedchars,
|
||||
}
|
||||
|
||||
if seed != 0 {
|
||||
instance.Rand = rand.New(rand.NewSource(seed))
|
||||
}else{
|
||||
instance.Rand = rand.New(rand.NewSource(time.Now().UnixMilli()))
|
||||
}
|
||||
instance.Mutex = &sync.Mutex{}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func (g *NonCryptoGenerator) GetString(length int) (string) {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num := g.Intn(len(g.characters))
|
||||
result[i] = g.characters[num]
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
129
pkg/utils/randomvalues/randomvalues.go
Normal file
129
pkg/utils/randomvalues/randomvalues.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package randomvalues
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/utils/mt19937"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
characters string
|
||||
counter int32
|
||||
maxUniform int32
|
||||
onceUniform sync.Once
|
||||
uniform *mt19937.UniformIntDistribution
|
||||
mt *mt19937.MT19937
|
||||
}
|
||||
|
||||
func NewGenerator(allowedchars string) Generator {
|
||||
if len(allowedchars) == 0 {
|
||||
allowedchars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
}
|
||||
instance := Generator{
|
||||
characters: allowedchars,
|
||||
maxUniform: 255,
|
||||
}
|
||||
val, _ := instance.GetUInt32()
|
||||
instance.counter = int32(val)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func (g *Generator) GetBytes(length int) ([]byte, error) {
|
||||
result := make([]byte, length)
|
||||
_, err := rand.Read(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (g *Generator) GetString(length int) (string, error) {
|
||||
result := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(g.characters))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result[i] = g.characters[num.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func (g *Generator) GetUInt64() (uint64, error) {
|
||||
buf := make([]byte, 8)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.LittleEndian.Uint64(buf), nil
|
||||
}
|
||||
|
||||
func (g *Generator) GetUInt32() (uint32, error) {
|
||||
buf := make([]byte, 4)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.LittleEndian.Uint32(buf), nil
|
||||
}
|
||||
|
||||
func (g *Generator) GetInt(max int) int {
|
||||
result, _ := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
return int(result.Int64())
|
||||
}
|
||||
|
||||
func (g *Generator) GetHex() (string, error) {
|
||||
value, err := g.GetUInt64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%016s", strconv.FormatUint(value, 16)), nil
|
||||
}
|
||||
|
||||
func (g *Generator) getUniformIntDistribution() *mt19937.UniformIntDistribution {
|
||||
g.onceUniform.Do(func() {
|
||||
if g.uniform != nil {
|
||||
return
|
||||
}
|
||||
g.mt = mt19937.New()
|
||||
g.uniform = mt19937.DistInt64(g.mt, 0, 255)
|
||||
})
|
||||
|
||||
return g.uniform
|
||||
}
|
||||
|
||||
func (g *Generator) GetUnformDistInt() int {
|
||||
dist := g.getUniformIntDistribution()
|
||||
return int(dist.Int64())
|
||||
}
|
||||
|
||||
func (g *Generator) GetUniformDistHex() (string, error) {
|
||||
now := time.Now().UnixNano()
|
||||
result := uint64(now) << 32
|
||||
d := g.GetUnformDistInt() & 0xFF
|
||||
result |= uint64(d << 24)
|
||||
d = g.GetUnformDistInt() & 0xFF
|
||||
result |= uint64(d << 16)
|
||||
c := g.counter & 0xFFFF
|
||||
result |= uint64(c)
|
||||
g.counter++
|
||||
|
||||
return fmt.Sprintf("%016s", strconv.FormatUint(result, 16)), nil
|
||||
}
|
||||
|
||||
func (g *Generator) Close() {
|
||||
g.counter = 0
|
||||
g.characters = ""
|
||||
g.mt = nil
|
||||
g.uniform = nil
|
||||
}
|
||||
200
pkg/utils/randomvalues/randomvalues_test.go
Normal file
200
pkg/utils/randomvalues/randomvalues_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package randomvalues_test
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"fiskerinc.com/modules/utils/randomvalues"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
type TestCase struct {
|
||||
Length int
|
||||
}
|
||||
|
||||
tests := []TestCase{
|
||||
{
|
||||
Length: 12,
|
||||
},
|
||||
{
|
||||
Length: 32,
|
||||
},
|
||||
{
|
||||
Length: 10,
|
||||
},
|
||||
{
|
||||
Length: 100,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name := fmt.Sprintf("%v Length", test.Length)
|
||||
result, err := instance.GetString(test.Length)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, nil, err)
|
||||
}
|
||||
if len(result) != test.Length {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, test.Length, len(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomUInt64(t *testing.T) {
|
||||
generated := map[uint64]int{}
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
value, err := instance.GetUInt64()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "RandomeUInt64", nil, err)
|
||||
}
|
||||
|
||||
if result, ok := generated[value]; ok {
|
||||
generated[value] = result + 1
|
||||
} else {
|
||||
generated[value] = 1
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range generated {
|
||||
if value > 1 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%v occurance", key), 1, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomHex(t *testing.T) {
|
||||
generated := map[string]int{}
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
value, err := instance.GetHex()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "RandomeHex", nil, err)
|
||||
}
|
||||
|
||||
if result, ok := generated[value]; ok {
|
||||
generated[value] = result + 1
|
||||
} else {
|
||||
generated[value] = 1
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range generated {
|
||||
if value > 1 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%v occurance", key), 1, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniformHex(t *testing.T) {
|
||||
repeats := 0
|
||||
|
||||
generated := map[string]int{}
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < 1000000; i++ {
|
||||
value, err := instance.GetUniformDistHex()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "UniformHex", nil, err)
|
||||
}
|
||||
|
||||
if result, ok := generated[value]; ok {
|
||||
generated[value] = result + 1
|
||||
repeats += 1
|
||||
} else {
|
||||
generated[value] = 1
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range generated {
|
||||
if value > 1 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%v occurance", key), 1, value)
|
||||
}
|
||||
}
|
||||
|
||||
if repeats > 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Repeated", 0, repeats)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomBytes(t *testing.T) {
|
||||
keysize := 16
|
||||
generated := map[string]int{}
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
value, err := instance.GetBytes(keysize)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "RandomBytes", nil, err)
|
||||
}
|
||||
|
||||
val := b64.StdEncoding.EncodeToString(value)
|
||||
if result, ok := generated[val]; ok {
|
||||
generated[val] = result + 1
|
||||
} else {
|
||||
generated[val] = 1
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range generated {
|
||||
if value > 1 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%v occurance", key), 1, value)
|
||||
}
|
||||
decoded, err := b64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Decode string error", nil, err)
|
||||
}
|
||||
if len(decoded) != keysize {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Decoded string length", keysize, len(decoded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomString(b *testing.B) {
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := instance.GetString(32)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomUInt64(b *testing.B) {
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := instance.GetUInt64()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomHex(b *testing.B) {
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := instance.GetHex()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniformDistHex(b *testing.B) {
|
||||
instance := randomvalues.NewGenerator("")
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := instance.GetUniformDistHex()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
pkg/utils/threadpool/callable.go
Normal file
35
pkg/utils/threadpool/callable.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package threadpool
|
||||
|
||||
// Callable the tasks which returns the output after exit should implement this interface
|
||||
type Callable interface {
|
||||
Call() interface{}
|
||||
}
|
||||
|
||||
// Future is the handle returned after submitting a callable task to the thread threadpool
|
||||
type Future struct {
|
||||
response chan interface{}
|
||||
done bool
|
||||
}
|
||||
|
||||
// callableTask is internally used to wrap the callable and future together
|
||||
// So that the worker can send the response back through channel provided in Future object
|
||||
type callableTask struct {
|
||||
Task Callable
|
||||
Handle *Future
|
||||
}
|
||||
|
||||
// Get returns the response of the Callable task when done
|
||||
// Is is the blocking call it waits for the execution to complete
|
||||
func (f *Future) Get() interface{} {
|
||||
return <-f.response
|
||||
}
|
||||
|
||||
// IsDone returns true if the execution is already done
|
||||
func (f *Future) IsDone() bool {
|
||||
return f.done
|
||||
}
|
||||
|
||||
// Runnable is interface for the jobs that will be executed by the threadpool
|
||||
type Runnable interface {
|
||||
Run()
|
||||
}
|
||||
85
pkg/utils/threadpool/threadpool.go
Normal file
85
pkg/utils/threadpool/threadpool.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package threadpool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrQueueFull = fmt.Errorf("queue is full, not able add the task")
|
||||
ErrNoWorkers = fmt.Errorf("worker pool is empty")
|
||||
)
|
||||
|
||||
type ThreadPool struct {
|
||||
workersTopLimit int
|
||||
workerPool chan chan interface{}
|
||||
closeHandle chan bool
|
||||
wgReceivers sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewThreadPool(workersLimit int) *ThreadPool {
|
||||
threadPool := &ThreadPool{workersTopLimit: workersLimit}
|
||||
threadPool.workerPool = make(chan chan interface{}, workersLimit)
|
||||
threadPool.closeHandle = make(chan bool)
|
||||
threadPool.wgReceivers = sync.WaitGroup{}
|
||||
threadPool.wgReceivers.Add(workersLimit)
|
||||
threadPool.createPool()
|
||||
return threadPool
|
||||
|
||||
}
|
||||
func (t *ThreadPool) Close() {
|
||||
close(t.closeHandle) // Stops all the routines
|
||||
t.wgReceivers.Wait()
|
||||
close(t.workerPool) // Closes the Job threadpool
|
||||
}
|
||||
|
||||
func (t *ThreadPool) createPool() {
|
||||
for i := 0; i < t.workersTopLimit; i++ {
|
||||
worker := NewWorker(t.workerPool, t.closeHandle, &t.wgReceivers)
|
||||
worker.Start()
|
||||
}
|
||||
go t.dispatch()
|
||||
}
|
||||
|
||||
func (t *ThreadPool) submitTask(task interface{}) error {
|
||||
// Add the task to the job queue
|
||||
//Find a worker for the job
|
||||
if t.workerPool == nil || t.workersTopLimit == 0 {
|
||||
return ErrNoWorkers
|
||||
}
|
||||
jobChannel := <-t.workerPool
|
||||
//Submit job to the worker
|
||||
jobChannel <- task
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute submits the job to available worker
|
||||
func (t *ThreadPool) Execute(task Runnable) error {
|
||||
return t.submitTask(task)
|
||||
}
|
||||
|
||||
// ExecuteFuture will submit the task to the threadpool and return the response handle
|
||||
func (t *ThreadPool) ExecuteFuture(task Callable) (*Future, error) {
|
||||
// Create future and task
|
||||
if t.workerPool == nil || t.workersTopLimit == 0 {
|
||||
return nil, ErrNoWorkers
|
||||
}
|
||||
handle := &Future{response: make(chan interface{})}
|
||||
futureTask := callableTask{Task: task, Handle: handle}
|
||||
err := t.submitTask(futureTask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return futureTask.Handle, nil
|
||||
}
|
||||
|
||||
// dispatch listens to the jobqueue and handles the jobs to the workers
|
||||
func (t *ThreadPool) dispatch() {
|
||||
for {
|
||||
select {
|
||||
case <-t.closeHandle:
|
||||
// Close thread threadpool
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
119
pkg/utils/threadpool/threadpool_test.go
Normal file
119
pkg/utils/threadpool/threadpool_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package threadpool
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
NumberOfWorkers = 20
|
||||
QueueSize = int64(1000)
|
||||
)
|
||||
|
||||
var (
|
||||
threadpool *ThreadPool
|
||||
)
|
||||
|
||||
func TestNewThreadPool(t *testing.T) {
|
||||
threadpool = NewThreadPool(NumberOfWorkers)
|
||||
}
|
||||
|
||||
func TestThreadPool_Execute(t *testing.T) {
|
||||
threadpool = NewThreadPool(NumberOfWorkers)
|
||||
data := &TestData{Val: "pristine"}
|
||||
task := &TestTask{TestData: data}
|
||||
threadpool.Execute(task)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
fmt.Println("")
|
||||
|
||||
if data.Val != "changed" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadPool_ExecuteFuture(t *testing.T) {
|
||||
threadpool = NewThreadPool(NumberOfWorkers)
|
||||
|
||||
task := &TestTaskFuture{}
|
||||
handle, _ := threadpool.ExecuteFuture(task)
|
||||
response := handle.Get()
|
||||
if !handle.IsDone() {
|
||||
t.Fail()
|
||||
}
|
||||
fmt.Println("Thread done ", response)
|
||||
}
|
||||
|
||||
func TestThreadPool_Close(t *testing.T) {
|
||||
threadpool = NewThreadPool(NumberOfWorkers)
|
||||
|
||||
threadpool.Close()
|
||||
}
|
||||
|
||||
func TestQueueFullError(t *testing.T) {
|
||||
threadpool = NewThreadPool(30)
|
||||
before := time.Now()
|
||||
|
||||
data := &TestData{Val: "pristine"}
|
||||
task := &TestTask{TestData: data}
|
||||
for i := 0; i < 30; i++ {
|
||||
err := threadpool.Execute(task)
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
threadpool.Close()
|
||||
after := time.Now()
|
||||
t.Logf("time start %d", after.Sub(before))
|
||||
t.Log("success")
|
||||
}
|
||||
|
||||
// func TestQueueFullError_Future(t *testing.T) {
|
||||
// threadpool = NewThreadPool(NumberOfWorkers)
|
||||
|
||||
// threadpool := NewThreadPool(1)
|
||||
|
||||
// task := &TestLongTaskFuture{}
|
||||
|
||||
// _, err := threadpool.ExecuteFuture(task)
|
||||
// if err != nil {
|
||||
// t.Fail()
|
||||
// }
|
||||
|
||||
// _, err = threadpool.ExecuteFuture(task)
|
||||
|
||||
// threadpool.Close()
|
||||
// }
|
||||
|
||||
type TestTask struct {
|
||||
TestData *TestData
|
||||
}
|
||||
|
||||
type TestData struct {
|
||||
Val string
|
||||
}
|
||||
|
||||
func (t *TestTask) Run() {
|
||||
time.Sleep(1 * time.Second)
|
||||
t.TestData.Val = "changed"
|
||||
}
|
||||
|
||||
type TestLongTask struct{}
|
||||
|
||||
func (t TestLongTask) Run() {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
type TestTaskFuture struct{}
|
||||
|
||||
func (t *TestTaskFuture) Call() interface{} {
|
||||
return "Done"
|
||||
}
|
||||
|
||||
type TestLongTaskFuture struct{}
|
||||
|
||||
func (t *TestLongTaskFuture) Call() interface{} {
|
||||
time.Sleep(5 * time.Second)
|
||||
return "Done"
|
||||
}
|
||||
60
pkg/utils/threadpool/worker.go
Normal file
60
pkg/utils/threadpool/worker.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package threadpool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Worker type holds the job channel and passed worker threadpool
|
||||
type Worker struct {
|
||||
jobChannel chan interface{}
|
||||
workerPool chan chan interface{}
|
||||
closeHandle chan bool
|
||||
receiver *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewWorker creates the new worker
|
||||
func NewWorker(workerPool chan chan interface{}, closeHandle chan bool, waitGroup *sync.WaitGroup) *Worker {
|
||||
|
||||
return &Worker{workerPool: workerPool,
|
||||
jobChannel: make(chan interface{}),
|
||||
closeHandle: closeHandle,
|
||||
receiver: waitGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker by listening to the job channel
|
||||
func (w Worker) Start() {
|
||||
go func() {
|
||||
defer w.receiver.Done()
|
||||
for {
|
||||
|
||||
// Put the worker to the worker threadpool
|
||||
w.workerPool <- w.jobChannel
|
||||
|
||||
select {
|
||||
// Wait for the job
|
||||
case job := <-w.jobChannel:
|
||||
// Got the job
|
||||
w.executeJob(job)
|
||||
case <-w.closeHandle:
|
||||
// Exit the go routine when the closeHandle channel is closed
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// executeJob executes the job based on the type
|
||||
func (w Worker) executeJob(job interface{}) {
|
||||
// Execute the job based on the task type
|
||||
switch task := job.(type) {
|
||||
case Runnable:
|
||||
task.Run()
|
||||
break
|
||||
case callableTask:
|
||||
response := task.Task.Call()
|
||||
task.Handle.done = true
|
||||
task.Handle.response <- response
|
||||
break
|
||||
}
|
||||
}
|
||||
18
pkg/utils/timehelper/time.go
Normal file
18
pkg/utils/timehelper/time.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package timehelper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const datetime64Layout = "%04d-%02d-%02dT%02d:%02d:%02d.%d"
|
||||
|
||||
func GetNow() *time.Time {
|
||||
now := time.Now()
|
||||
return &now
|
||||
}
|
||||
|
||||
func FormatDateime64(timestamp time.Time) string {
|
||||
utc := timestamp.UTC()
|
||||
return fmt.Sprintf(datetime64Layout, utc.Year(), utc.Month(), utc.Day(), utc.Hour(), utc.Minute(), utc.Second(), utc.Nanosecond()/1000)
|
||||
}
|
||||
69
pkg/utils/urlhelper/urlhelper.go
Normal file
69
pkg/utils/urlhelper/urlhelper.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package urlhelper
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BuildQuery builds querystring from map
|
||||
func BuildQuery(queries map[string]string) string {
|
||||
var qs []string
|
||||
|
||||
for key, value := range queries {
|
||||
qs = append(qs, key+"="+url.QueryEscape(value))
|
||||
}
|
||||
|
||||
return strings.Join(qs, "&")
|
||||
}
|
||||
|
||||
// BuildURL builds url with base url and querystring
|
||||
func BuildURL(base string, queries map[string]string) string {
|
||||
return strings.Join([]string{base, BuildQuery(queries)}, "?")
|
||||
}
|
||||
|
||||
func GetQueryInt(query url.Values, key string) int {
|
||||
i, _ := strconv.Atoi(query.Get(key))
|
||||
return i
|
||||
}
|
||||
|
||||
// Parse a epoch unix time number from the URL
|
||||
func GetQueryUnix(query url.Values, key string) (t time.Time) {
|
||||
return time.UnixMilli(GetQueryInt64(query, key))
|
||||
}
|
||||
|
||||
func GetQueryTimeStamp(query url.Values, key string) (t time.Time) {
|
||||
t, _ = time.Parse(time.RFC3339Nano, query.Get(key))
|
||||
return t
|
||||
}
|
||||
|
||||
// GetQueryBool returns val, ok.
|
||||
func GetQueryBool(query url.Values, key string) (bool, bool) {
|
||||
b, err := strconv.ParseBool(query.Get(key))
|
||||
return b, err == nil
|
||||
}
|
||||
|
||||
func GetQueryInt64(query url.Values, key string) int64 {
|
||||
i, _ := strconv.ParseInt(query.Get(key), 0, 64)
|
||||
return i
|
||||
}
|
||||
|
||||
func GetQueryFloat32(query url.Values, key string) float32 {
|
||||
i, _ := strconv.ParseFloat(query.Get(key), 32)
|
||||
return float32(i)
|
||||
}
|
||||
|
||||
func GetQueryUUID(query url.Values, key string) uuid.UUID {
|
||||
val := query.Get(key)
|
||||
if val != "" {
|
||||
id, err := uuid.Parse(val)
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
return uuid.Nil
|
||||
}
|
||||
72
pkg/utils/urlhelper/urlhelper_test.go
Normal file
72
pkg/utils/urlhelper/urlhelper_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package urlhelper
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func checkQueryString(t *testing.T, querystring string, data map[string]string) {
|
||||
for key, value := range data {
|
||||
compare := strings.Join([]string{key, url.QueryEscape(value)}, "=")
|
||||
if strings.Index(querystring, compare) == -1 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestBuildQuery", compare, querystring)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuery(t *testing.T) {
|
||||
qs := map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "https://www.fiskerinc.com/?c=C",
|
||||
}
|
||||
|
||||
result := BuildQuery(qs)
|
||||
checkQueryString(t, result, qs)
|
||||
}
|
||||
|
||||
func TestBuildURL(t *testing.T) {
|
||||
domain := "https://testing.com"
|
||||
qs := map[string]string{
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "https://www.fiskerinc.com/?c=C",
|
||||
}
|
||||
|
||||
result := BuildURL("https://testing.com", qs)
|
||||
|
||||
if strings.Index(result, domain+"?") != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestBuildURL", domain, result)
|
||||
}
|
||||
|
||||
checkQueryString(t, result, qs)
|
||||
}
|
||||
|
||||
func TestGetQueryInt(t *testing.T) {
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://example.com?limit=50&offset=5&text=XXXXXX", nil)
|
||||
q := r.URL.Query()
|
||||
|
||||
i := GetQueryInt(q, "nonexistent")
|
||||
if i != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Non-existing query", 0, i)
|
||||
}
|
||||
|
||||
i = GetQueryInt(q, "text")
|
||||
if i != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Text query", 0, i)
|
||||
}
|
||||
|
||||
i = GetQueryInt(q, "limit")
|
||||
if i != 50 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Limit query", 50, i)
|
||||
}
|
||||
|
||||
i = GetQueryInt(q, "offset")
|
||||
if i != 5 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "Offset query", 5, i)
|
||||
}
|
||||
}
|
||||
126
pkg/utils/vin_parser.go
Normal file
126
pkg/utils/vin_parser.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/validator"
|
||||
"fiskerinc.com/modules/vindecoder"
|
||||
"github.com/pkg/errors"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultCountry = envtool.GetEnv("DEFAULT_VEH_COUNTRY", "Unknown")
|
||||
defaultModel = envtool.GetEnv("DEFAULT_VEH_MODEL", "Ocean")
|
||||
defaultYear = envtool.GetEnvInt("DEFAULT_VEH_YEAR", 2022)
|
||||
defaultTrim = envtool.GetEnv("DEFAULT_VEH_TRIM", "Unknown")
|
||||
defaultPowertrain = envtool.GetEnv("DEFAULT_VEH_POWERTRAIN", "Unknown")
|
||||
defaultRestraint = envtool.GetEnv("DEFAULT_VEH_RESTRAINT", "Unknown")
|
||||
defaultBodyType = envtool.GetEnv("DEFAULT_VEH_BODY_TYPE", "Unknown")
|
||||
)
|
||||
|
||||
func ParseVIN(vin string) (*common.Car, error) {
|
||||
if vin == "" {
|
||||
return nil, errors.Errorf("vin is empty")
|
||||
}
|
||||
|
||||
vinInfo, _ := vindecoder.DecodeVIN(vin)
|
||||
|
||||
// for all vins, do a simple regex validation
|
||||
valid := validator.ValidateVINSimple(vin)
|
||||
if !valid {
|
||||
return nil, errors.Errorf("vin %v is invalid", vin)
|
||||
}
|
||||
|
||||
// for all vins, do a checksum validation
|
||||
if !vinInfo.IsValid {
|
||||
return nil, errors.Errorf("vin %v is invalid", vin)
|
||||
}
|
||||
|
||||
var defaultRegion common.RegionCode
|
||||
switch defaultRestraint {
|
||||
case "US Specs":
|
||||
defaultRegion = common.US
|
||||
case "EU Specs":
|
||||
defaultRegion = common.EU
|
||||
default:
|
||||
defaultRegion = common.US
|
||||
}
|
||||
|
||||
if vinInfo.Manufacturer != "Fisker GmbH" {
|
||||
// for non-Fisker vins, use default values
|
||||
return &common.Car{
|
||||
VIN: vin,
|
||||
Region: defaultRegion,
|
||||
Country: defaultCountry,
|
||||
Model: defaultModel,
|
||||
Trim: defaultTrim,
|
||||
Year: defaultYear,
|
||||
Powertrain: defaultPowertrain,
|
||||
Restraint: defaultRestraint,
|
||||
BodyType: defaultBodyType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if vinInfo.Country == "" {
|
||||
vinInfo.Country = defaultCountry
|
||||
}
|
||||
if vinInfo.Model == "" {
|
||||
vinInfo.Model = defaultModel
|
||||
}
|
||||
if vinInfo.Trim == "" {
|
||||
vinInfo.Trim = defaultTrim
|
||||
}
|
||||
if vinInfo.Powertrain == "" {
|
||||
vinInfo.Powertrain = defaultPowertrain
|
||||
}
|
||||
if vinInfo.Restraint == "" {
|
||||
vinInfo.Restraint = defaultRestraint
|
||||
}
|
||||
if vinInfo.BodyType == "" {
|
||||
vinInfo.BodyType = defaultBodyType
|
||||
}
|
||||
|
||||
var region common.RegionCode
|
||||
switch vinInfo.Restraint {
|
||||
case "US Specs":
|
||||
region = common.US
|
||||
case "EU Specs":
|
||||
region = common.EU
|
||||
}
|
||||
|
||||
return &common.Car{
|
||||
VIN: vin,
|
||||
Region: region,
|
||||
Country: vinInfo.Country,
|
||||
Model: vinInfo.Model,
|
||||
Trim: vinInfo.Trim,
|
||||
Year: vinInfo.Year,
|
||||
Powertrain: vinInfo.Powertrain,
|
||||
Restraint: vinInfo.Restraint,
|
||||
BodyType: vinInfo.BodyType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseVINs(vins []string) ([]*common.Car, error) {
|
||||
if len(vins) == 0 {
|
||||
return nil, errors.Errorf("vin list is empty")
|
||||
}
|
||||
|
||||
var cars []*common.Car
|
||||
var invalidVINs []string
|
||||
|
||||
for _, vin := range vins {
|
||||
car, err := ParseVIN(vin)
|
||||
if err != nil {
|
||||
invalidVINs = append(invalidVINs, vin)
|
||||
} else {
|
||||
cars = append(cars, car)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidVINs) > 0 {
|
||||
return cars, errors.Errorf("vins %+q are invalid", invalidVINs)
|
||||
}
|
||||
|
||||
return cars, nil
|
||||
}
|
||||
192
pkg/utils/vin_parser_test.go
Normal file
192
pkg/utils/vin_parser_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseVIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testVin string
|
||||
expectedCar *common.Car
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "empty vin",
|
||||
testVin: "",
|
||||
expectedCar: nil,
|
||||
expectedErr: errors.Errorf("vin is empty"),
|
||||
},
|
||||
{
|
||||
name: "invalid vin",
|
||||
testVin: "XXXXXXXXXXXXXXXXX",
|
||||
expectedCar: nil,
|
||||
expectedErr: errors.Errorf("vin XXXXXXXXXXXXXXXXX is invalid"),
|
||||
},
|
||||
{
|
||||
name: "invalid fisker vin",
|
||||
testVin: "VCFQQQQQQQQQQQQQQ",
|
||||
expectedCar: nil,
|
||||
expectedErr: errors.Errorf("vin VCFQQQQQQQQQQQQQQ is invalid"),
|
||||
},
|
||||
{
|
||||
name: "invalid checksum Fisker vin",
|
||||
testVin: "VCF1ZBU28PG159581",
|
||||
expectedCar: nil,
|
||||
expectedErr: errors.Errorf("vin VCF1ZBU28PG159581 is invalid"),
|
||||
},
|
||||
{
|
||||
name: "non-fisker vin",
|
||||
testVin: "1G1FP87S3GN100062",
|
||||
expectedCar: &common.Car{
|
||||
VIN: "1G1FP87S3GN100062",
|
||||
Country: defaultCountry,
|
||||
Model: defaultModel,
|
||||
Trim: defaultTrim,
|
||||
Year: defaultYear,
|
||||
Powertrain: defaultPowertrain,
|
||||
Restraint: defaultRestraint,
|
||||
BodyType: defaultBodyType,
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "fisker vin",
|
||||
testVin: "VCF1EBE25PG001013",
|
||||
expectedCar: &common.Car{
|
||||
VIN: "VCF1EBE25PG001013",
|
||||
Country: "Austria",
|
||||
Model: "Ocean",
|
||||
Trim: "Extreme",
|
||||
Year: 2023,
|
||||
Powertrain: "LBP/DM/AWD",
|
||||
Restraint: "EU Specs",
|
||||
BodyType: "5-Door MPV, 5-Seater, Class E",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Log(test.name)
|
||||
|
||||
car, err := ParseVIN(test.testVin)
|
||||
|
||||
if test.expectedErr != nil {
|
||||
assert.Equal(t, test.expectedErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
if test.expectedCar != nil {
|
||||
assert.Equal(t, test.expectedCar.VIN, car.VIN)
|
||||
assert.Equal(t, test.expectedCar.Country, car.Country)
|
||||
assert.Equal(t, test.expectedCar.Model, car.Model)
|
||||
assert.Equal(t, test.expectedCar.Trim, car.Trim)
|
||||
assert.Equal(t, test.expectedCar.Year, car.Year)
|
||||
assert.Equal(t, test.expectedCar.Powertrain, car.Powertrain)
|
||||
assert.Equal(t, test.expectedCar.Restraint, car.Restraint)
|
||||
assert.Equal(t, test.expectedCar.BodyType, car.BodyType)
|
||||
} else {
|
||||
assert.Nil(t, car)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVINs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testVins []string
|
||||
expectedCars []*common.Car
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "valid vin",
|
||||
testVins: []string{"VCF1UBE22PG888888"},
|
||||
expectedCars: []*common.Car{{
|
||||
VIN: "VCF1UBE22PG888888",
|
||||
Country: "Austria",
|
||||
Model: "Ocean",
|
||||
Trim: "Extreme",
|
||||
Year: 2023,
|
||||
Powertrain: "LBP/DM/AWD",
|
||||
Restraint: "EU Specs",
|
||||
BodyType: "5-Door MPV, 5-Seater, Class E",
|
||||
}},
|
||||
expectedErr: errors.Errorf(`vins ["VCF1UBE22PG888888"] are invalid`),
|
||||
},
|
||||
{
|
||||
name: "empty vin list",
|
||||
testVins: nil,
|
||||
expectedCars: nil,
|
||||
expectedErr: errors.Errorf("vin list is empty"),
|
||||
},
|
||||
{
|
||||
name: "invalid vin",
|
||||
testVins: []string{"XXXXXXXXXXXXXXXXX"},
|
||||
expectedCars: nil,
|
||||
expectedErr: errors.Errorf("vins [\"XXXXXXXXXXXXXXXXX\"] are invalid"),
|
||||
},
|
||||
{
|
||||
name: "valid vin",
|
||||
testVins: []string{"VCF1EBE25PG001013"},
|
||||
expectedCars: []*common.Car{{
|
||||
VIN: "VCF1EBE25PG001013",
|
||||
Country: "Austria",
|
||||
Model: "Ocean",
|
||||
Trim: "Extreme",
|
||||
Year: 2023,
|
||||
Powertrain: "LBP/DM/AWD",
|
||||
Restraint: "EU Specs",
|
||||
BodyType: "5-Door MPV, 5-Seater, Class E",
|
||||
}},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "partially valid vins",
|
||||
testVins: []string{"VCF1EBE25PG001013", "XXXXXXXXXXXXXXXXX"},
|
||||
expectedCars: []*common.Car{{
|
||||
VIN: "VCF1EBE25PG001013",
|
||||
Country: "Austria",
|
||||
Model: "Ocean",
|
||||
Trim: "Extreme",
|
||||
Year: 2023,
|
||||
Powertrain: "LBP/DM/AWD",
|
||||
Restraint: "EU Specs",
|
||||
BodyType: "5-Door MPV, 5-Seater, Class E",
|
||||
}},
|
||||
expectedErr: errors.Errorf("vins [\"XXXXXXXXXXXXXXXXX\"] are invalid"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Log(test.name)
|
||||
|
||||
cars, err := ParseVINs(test.testVins)
|
||||
|
||||
if test.expectedErr != nil {
|
||||
assert.Equal(t, test.expectedErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
for i, car := range cars {
|
||||
if test.expectedCars[i] != nil {
|
||||
assert.Equal(t, test.expectedCars[i].VIN, car.VIN)
|
||||
assert.Equal(t, test.expectedCars[i].Country, car.Country)
|
||||
assert.Equal(t, test.expectedCars[i].Model, car.Model)
|
||||
assert.Equal(t, test.expectedCars[i].Trim, car.Trim)
|
||||
assert.Equal(t, test.expectedCars[i].Year, car.Year)
|
||||
assert.Equal(t, test.expectedCars[i].Powertrain, car.Powertrain)
|
||||
assert.Equal(t, test.expectedCars[i].Restraint, car.Restraint)
|
||||
assert.Equal(t, test.expectedCars[i].BodyType, car.BodyType)
|
||||
} else {
|
||||
assert.Nil(t, car)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
pkg/utils/vod/crc.go
Normal file
68
pkg/utils/vod/crc.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package vod
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/sigurn/crc8"
|
||||
)
|
||||
|
||||
func NewVODHelper(lengthInCRC bool,
|
||||
crcInLength bool,
|
||||
lengthInLength bool) VODHelper {
|
||||
return VODHelper{
|
||||
table: crc8.MakeTable(crc8.Params{
|
||||
Poly: 0x1D,
|
||||
Init: 0,
|
||||
RefIn: false,
|
||||
RefOut: false,
|
||||
XorOut: 0,
|
||||
Check: 0,
|
||||
Name: "CEC-8 VOD",
|
||||
}),
|
||||
lengthInCRC: lengthInCRC,
|
||||
crcInLength: crcInLength,
|
||||
lengthInLength: lengthInLength,
|
||||
}
|
||||
}
|
||||
|
||||
type VODHelper struct {
|
||||
table *crc8.Table
|
||||
lengthInCRC bool
|
||||
crcInLength bool
|
||||
lengthInLength bool
|
||||
}
|
||||
|
||||
// Given a list of bytes, we calculate our custom CRC-8 on it, then prepend the length and postpend the crc in place of last byte
|
||||
// So 0x01 0x02 0x00 -> 0x00 0x05 0x01 0x02 0x00 0xCRC-8(0x01 0x02 0x00)
|
||||
// If you have an existing VOD that you are modifying, this function expects you to have removed the length and the crc
|
||||
func (v *VODHelper) AddLengthAndCRC(data []byte) []byte {
|
||||
// first 2 bytes are length including length bytes
|
||||
length := make([]byte, 2)
|
||||
lengthPlus := 0
|
||||
if v.crcInLength { // No
|
||||
lengthPlus += 1
|
||||
}
|
||||
if v.lengthInLength { // Yes
|
||||
lengthPlus += 2
|
||||
}
|
||||
binary.BigEndian.PutUint16(length, uint16(len(data)+lengthPlus))
|
||||
|
||||
var crc byte
|
||||
if v.lengthInCRC { // no
|
||||
data = append(length, data...)
|
||||
// calculate crc on data only
|
||||
crc = v.GenerateCRC(data)
|
||||
} else { // yes
|
||||
// calculate crc on data only
|
||||
crc = v.GenerateCRC(data)
|
||||
data = append(length, data...)
|
||||
}
|
||||
data = append(data, crc)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (v *VODHelper) GenerateCRC(data []byte) byte {
|
||||
crc := crc8.Checksum(data, v.table)
|
||||
return crc
|
||||
}
|
||||
133
pkg/utils/vod/crc_test.go
Normal file
133
pkg/utils/vod/crc_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package vod
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCustomCRC(t *testing.T) {
|
||||
inputString := "2301084000000101012200010101010001010101000000000000000000FF7EFF7F000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100000101020101010101010000000000000000FFFFFF0000000201010202000101"
|
||||
|
||||
inputB, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
p := NewVODHelper(false, false, false)
|
||||
output := p.GenerateCRC(inputB)
|
||||
if output != 0x5F {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoesMatchReport(t *testing.T) {
|
||||
reported := "00A92301084000000101012200010101010001010101000000000000000000FF7EFF7F000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100000101020101010101010000000000000000FFFFFF00000002010102020001015F"
|
||||
reportedBytes, err := hex.DecodeString(reported)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
inputBytes := reportedBytes[2 : len(reportedBytes)-1]
|
||||
p := NewVODHelper(false, false, true)
|
||||
outputBytes := p.AddLengthAndCRC(inputBytes)
|
||||
|
||||
if len(reportedBytes) != len(outputBytes) {
|
||||
t.Logf("Lengths did not match %d %d", len(reportedBytes), len(outputBytes))
|
||||
t.Fail()
|
||||
}
|
||||
if !bytes.Equal(outputBytes, reportedBytes) {
|
||||
t.Log("input and output did not match")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLengthOfFinal(t *testing.T) {
|
||||
inputString := "2EF1110102030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798990001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626399"
|
||||
inputB, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Expect the length to be increased by 2 from pre-pending length
|
||||
p := NewVODHelper(false, false, true)
|
||||
output := p.AddLengthAndCRC(inputB)
|
||||
if len(output) != len(inputB)+3 {
|
||||
t.Logf("Expected a final length of %d but got %d", len(inputB)+3, len(output))
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
length := binary.BigEndian.Uint16(output[:2])
|
||||
// Don't include crc with length
|
||||
if int64(length) != int64(len(output)-1) {
|
||||
t.Logf("The calculated length does not match actual length %d %d", int64(length), int64(len(output)-1))
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRCCorrect(t *testing.T) {
|
||||
inputString := "00112233"
|
||||
inputB, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Expect the length to be increased by 2 from pre-pending length
|
||||
p := NewVODHelper(false, true, false)
|
||||
output := p.GenerateCRC(inputB)
|
||||
secondStep := append(inputB, output)
|
||||
|
||||
output2 := p.GenerateCRC(secondStep)
|
||||
if output2 != 0x00 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRCCorrect2(t *testing.T) {
|
||||
inputString := "00112233"
|
||||
inputB, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Expect the length to be increased by 2 from pre-pending length
|
||||
p := NewVODHelper(false, false, false)
|
||||
output := p.AddLengthAndCRC(inputB)
|
||||
// Chopping out length
|
||||
output = output[2:]
|
||||
output2 := p.GenerateCRC(output)
|
||||
if output2 != 0x00 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRCTotalZero(t *testing.T) {
|
||||
p := NewVODHelper(false, false, true)
|
||||
inputString := "030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798990001020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626399"
|
||||
inputB, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
crc := p.GenerateCRC(inputB)
|
||||
if crc != 0x00 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRCAndLengthSame(t *testing.T) {
|
||||
inputString := "01012301084000000101012200010101010001010101000000000000000000FF7EFF7F000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100000101020101010101010000000000000000FFFFFF000101020101020200010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000BA"
|
||||
inputBytes, err := hex.DecodeString(inputString)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
//Removing the length and the CRC
|
||||
p := NewVODHelper(false, false, true)
|
||||
choppedInput := inputBytes[2 : len(inputBytes)-1]
|
||||
choppedInput = p.AddLengthAndCRC(choppedInput)
|
||||
if !bytes.Equal(inputBytes, choppedInput) {
|
||||
t.Log("CRC and length not calculated as expected")
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
60
pkg/utils/whereami/where_am_i.go
Normal file
60
pkg/utils/whereami/where_am_i.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package whereami
|
||||
|
||||
import "fiskerinc.com/modules/utils/envtool"
|
||||
|
||||
var (
|
||||
Environment serviceEnvironment = serviceEnvironment(envtool.GetEnv("APP_SERVICE_ENVIRONMENT", ""))
|
||||
Service serviceName = serviceName(envtool.GetEnv("APP_SERVICE_NAME", ""))
|
||||
)
|
||||
|
||||
func SetEnvironment(env serviceEnvironment) {
|
||||
Environment = env
|
||||
}
|
||||
|
||||
func SetService(srv serviceName) {
|
||||
Service = srv
|
||||
}
|
||||
|
||||
type serviceName string
|
||||
|
||||
const (
|
||||
AFTERSALES serviceName = "AFTERSALES"
|
||||
ATTENDANT serviceName = "ATTENDANT"
|
||||
AUTH serviceName = "COMPUTE_AUTH"
|
||||
BEACON serviceName = "BEACON"
|
||||
CARGO serviceName = "CARGO"
|
||||
CERT serviceName = "CERT"
|
||||
CERTINSTALL serviceName = "CERTINSTALL"
|
||||
CHARGESIMULATOR serviceName = "CHARGESIMULATOR"
|
||||
CONSUMER_WEB_CONNECT serviceName = "CONSUMER_WEB_CONNECT"
|
||||
DEPOT serviceName = "DEPOT"
|
||||
DITTO serviceName = "DITTO"
|
||||
EXTERNALAPI serviceName = "EXTERNALAPI"
|
||||
GATEWAY serviceName = "GATEWAY"
|
||||
JETFIRE serviceName = "JETFIRE"
|
||||
KEYGEN serviceName = "KEYGEN"
|
||||
MANUFACTURE serviceName = "MANUFACTURE"
|
||||
MEGATRON serviceName = "MEGATRON"
|
||||
ML_EVENT_DETECTION serviceName = "ML_EVENT_DETECTION"
|
||||
NOTIFIER serviceName = "NOTIFIER"
|
||||
OPTIMUS serviceName = "OPTIMUS"
|
||||
OTA serviceName = "OTA"
|
||||
SMS_SERVICE serviceName = "SMS_SERVICE"
|
||||
SUBSCRIPTION serviceName = "SUBSCRIPTION"
|
||||
TIMEZONE serviceName = "TIMEZONE"
|
||||
TOMTOM serviceName = "TOMTOM"
|
||||
TREX_LOG serviceName = "TREX_LOG"
|
||||
VALET serviceName = "VALET"
|
||||
VEHICLEAPI serviceName = "VEHICLEAPI"
|
||||
)
|
||||
|
||||
type serviceEnvironment string
|
||||
|
||||
const (
|
||||
PRODUCTION serviceEnvironment = "PRODUCTION"
|
||||
PRODUCTION_EU serviceEnvironment = "PRODUCTION_EU"
|
||||
PRE_PRODUCTION serviceEnvironment = "PRE_PRODUCTION"
|
||||
STAGE serviceEnvironment = "STAGE"
|
||||
DEVELOPMENT serviceEnvironment = "DEVELOPMENT"
|
||||
LOCAL serviceEnvironment = "LOCAL"
|
||||
)
|
||||
30
pkg/utils/whereami/where_am_i_test.go
Normal file
30
pkg/utils/whereami/where_am_i_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package whereami_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/utils/whereami"
|
||||
)
|
||||
|
||||
func TestNoENV(t *testing.T){
|
||||
if whereami.Environment != ""{
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if whereami.Service != ""{
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestManuallySet(t *testing.T){
|
||||
whereami.SetEnvironment(whereami.LOCAL)
|
||||
whereami.SetService(whereami.CARGO)
|
||||
|
||||
if whereami.Environment != whereami.LOCAL {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if whereami.Service != whereami.CARGO {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
31
pkg/utils/xml_resp.go
Normal file
31
pkg/utils/xml_resp.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
)
|
||||
|
||||
// ErrorXMLResult makes error result
|
||||
func ErrorXMLResult(status int, message string) common.XMLError {
|
||||
return common.XMLError{
|
||||
Error: http.StatusText(status),
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// RespXML sends back XML response.
|
||||
func RespXML(w http.ResponseWriter, status int, resp interface{}) {
|
||||
js, _ := xml.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
// RespXMLError XML error response
|
||||
func RespXMLError(w http.ResponseWriter, status int, message string) {
|
||||
resp := ErrorXMLResult(status, message)
|
||||
RespXML(w, status, &resp)
|
||||
}
|
||||
Reference in New Issue
Block a user