Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

28
pkg/utils/app/app.go Normal file
View 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
View 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
}

View 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)
}
}

View 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)
}

View 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 ""
}

View 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
View File

@@ -0,0 +1,5 @@
package elptr
func ElPtr[T any](e T) *T {
return &e
}

64
pkg/utils/envtool/env.go Normal file
View 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
}

View 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
View 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
View 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
}

View 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
View 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)
}

View 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)
}
}

View 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
View 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
}

View 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))
}
}

View 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
}
}

View 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
}

View 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))
}

View 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
}

View 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
}

View 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
}

View 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),
}
}

View 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)
}
})
}
}

View 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
}

View 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)
}

View 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)
}

View 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
}

View 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)
}
}
}

View 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()
}

View 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
}
}
}

View 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"
}

View 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
}
}

View 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)
}

View 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
}

View 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
View 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
}

View 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
View 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
View 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()
}
}

View 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"
)

View 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
View 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)
}