Compare commits
15 Commits
main
...
essential/
| Author | SHA1 | Date | |
|---|---|---|---|
| db9f47b9a7 | |||
| 7cb56c8fbb | |||
| 426f224c5c | |||
| 05e66b30b1 | |||
| aa9030d5b9 | |||
| 671a6c43f9 | |||
| 3475a91782 | |||
| e0d950b130 | |||
| f6694cb17d | |||
| 9611622ab3 | |||
| ebc7dc3800 | |||
| c61eb950fb | |||
| 5056835dcb | |||
| 92675e8ad2 | |||
| 17fb76debe |
@ -0,0 +1,514 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Clock
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provides the clock synchronization service for maintaining a
|
||||||
|
| consistent system time reference across automation tasks.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package clock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimClockSoftwareName = "obrim"
|
||||||
|
|
||||||
|
obrimClockNTPPort = 123
|
||||||
|
obrimClockNTPPacketSize = 48
|
||||||
|
obrimClockNTPVersion = 4
|
||||||
|
obrimClockNTPClientMode = 3
|
||||||
|
obrimClockNTPUnixOffset = 2208988800
|
||||||
|
obrimClockMaxRTT = 2 * time.Second
|
||||||
|
obrimClockSynchronizationInterval = 15 * time.Minute
|
||||||
|
obrimClockServerTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
obrimClockSuccess = 0
|
||||||
|
obrimClockFailure = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
obrimClockMutex sync.RWMutex
|
||||||
|
obrimClockStop chan struct{}
|
||||||
|
obrimClockDone chan struct{}
|
||||||
|
|
||||||
|
obrimClockOffset int64
|
||||||
|
obrimClockLastSync int64
|
||||||
|
|
||||||
|
obrimClockStarted bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// obrimClockConfig represents the persistent clock configuration.
|
||||||
|
type obrimClockConfig struct {
|
||||||
|
Clock *obrimClockState `json:"clock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockFile represents the persistent configuration document.
|
||||||
|
type obrimClockFile struct {
|
||||||
|
Config *obrimClockConfig `json:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockState represents the runtime and persistent synchronization state.
|
||||||
|
type obrimClockState struct {
|
||||||
|
ClockOffset int64 `json:"clockOffset"`
|
||||||
|
LastSync int64 `json:"lastSync"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockMeasurement represents one accepted or rejected NTP measurement.
|
||||||
|
type obrimClockMeasurement struct {
|
||||||
|
ServerTime time.Time
|
||||||
|
RequestTime time.Time
|
||||||
|
ResponseTime time.Time
|
||||||
|
RTT time.Duration
|
||||||
|
Offset time.Duration
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockResponse represents the timestamps extracted from an NTP response.
|
||||||
|
type obrimClockResponse struct {
|
||||||
|
ReceiveTimestamp uint64
|
||||||
|
TransmitTimestamp uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimClockStart starts the clock synchronization goroutine.
|
||||||
|
func ObrimClockStart() {
|
||||||
|
obrimClockMutex.Lock()
|
||||||
|
|
||||||
|
if obrimClockStarted {
|
||||||
|
obrimClockMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimClockStop = make(chan struct{})
|
||||||
|
obrimClockDone = make(chan struct{})
|
||||||
|
obrimClockStarted = true
|
||||||
|
|
||||||
|
obrimClockInitializeClock()
|
||||||
|
|
||||||
|
stop := obrimClockStop
|
||||||
|
done := obrimClockDone
|
||||||
|
|
||||||
|
obrimClockMutex.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
obrimClockSynchronizeClock()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(obrimClockSynchronizationInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
obrimClockSynchronizeClock()
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimClockStop gracefully stops the clock synchronization goroutine.
|
||||||
|
func ObrimClockStop() {
|
||||||
|
obrimClockMutex.Lock()
|
||||||
|
|
||||||
|
if !obrimClockStarted {
|
||||||
|
obrimClockMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := obrimClockStop
|
||||||
|
done := obrimClockDone
|
||||||
|
|
||||||
|
obrimClockStop = nil
|
||||||
|
obrimClockDone = nil
|
||||||
|
obrimClockStarted = false
|
||||||
|
|
||||||
|
obrimClockMutex.Unlock()
|
||||||
|
|
||||||
|
close(stop)
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockInitializeClock initializes runtime clock state from persistent storage.
|
||||||
|
func obrimClockInitializeClock() {
|
||||||
|
state, result := obrimClockLoadClock()
|
||||||
|
|
||||||
|
obrimClockMutex.Lock()
|
||||||
|
defer obrimClockMutex.Unlock()
|
||||||
|
|
||||||
|
if result == obrimClockSuccess && state != nil {
|
||||||
|
obrimClockOffset = state.ClockOffset
|
||||||
|
obrimClockLastSync = state.LastSync
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimClockOffset = 0
|
||||||
|
obrimClockLastSync = 0
|
||||||
|
|
||||||
|
_ = obrimClockPersistClock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockSynchronizeClock performs one NTP synchronization cycle.
|
||||||
|
func obrimClockSynchronizeClock() {
|
||||||
|
servers := []string{
|
||||||
|
"time.cloudflare.com",
|
||||||
|
"time.google.com",
|
||||||
|
"pool.ntp.org",
|
||||||
|
}
|
||||||
|
|
||||||
|
measurements := make([]obrimClockMeasurement, 0, len(servers))
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
measurement, result := obrimClockQueryServer(server)
|
||||||
|
|
||||||
|
if result != obrimClockSuccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimClockMeasureDelay(&measurement) != obrimClockSuccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimClockCalculateOffset(&measurement) != obrimClockSuccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimClockValidateMeasurement(&measurement) != obrimClockSuccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
measurements = append(measurements, measurement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(measurements) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, result := obrimClockCalculateAverageOffset(measurements)
|
||||||
|
|
||||||
|
if result != obrimClockSuccess {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clockOffset := offset.Nanoseconds()
|
||||||
|
lastSync := time.Now().UnixNano()
|
||||||
|
|
||||||
|
obrimClockUpdateClock(clockOffset, lastSync)
|
||||||
|
_ = obrimClockPersistClock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockQueryServer sends an NTP request and receives a server response.
|
||||||
|
func obrimClockQueryServer(server string) (obrimClockMeasurement, int) {
|
||||||
|
var measurement obrimClockMeasurement
|
||||||
|
|
||||||
|
address := net.JoinHostPort(server, "123")
|
||||||
|
|
||||||
|
connection, err := net.DialTimeout("udp", address, obrimClockServerTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
|
||||||
|
request := make([]byte, obrimClockNTPPacketSize)
|
||||||
|
request[0] = (obrimClockNTPVersion << 3) | obrimClockNTPClientMode
|
||||||
|
|
||||||
|
requestTime := time.Now()
|
||||||
|
|
||||||
|
if _, err := connection.Write(request); err != nil {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := connection.SetReadDeadline(time.Now().Add(obrimClockServerTimeout)); err != nil {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make([]byte, obrimClockNTPPacketSize)
|
||||||
|
|
||||||
|
if _, err := connection.Read(response); err != nil {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
responseTime := time.Now()
|
||||||
|
|
||||||
|
if len(response) < obrimClockNTPPacketSize {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveTimestamp := binary.BigEndian.Uint64(response[32:40])
|
||||||
|
transmitTimestamp := binary.BigEndian.Uint64(response[40:48])
|
||||||
|
|
||||||
|
if transmitTimestamp == 0 {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
serverTime, err := obrimClockNTPToTime(transmitTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
return measurement, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement = obrimClockMeasurement{
|
||||||
|
ServerTime: serverTime,
|
||||||
|
RequestTime: requestTime,
|
||||||
|
ResponseTime: responseTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = receiveTimestamp
|
||||||
|
|
||||||
|
return measurement, obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockMeasureDelay calculates network round-trip time.
|
||||||
|
func obrimClockMeasureDelay(measurement *obrimClockMeasurement) int {
|
||||||
|
if measurement == nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if measurement.ResponseTime.Before(measurement.RequestTime) {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement.RTT = measurement.ResponseTime.Sub(measurement.RequestTime)
|
||||||
|
|
||||||
|
if measurement.RTT <= 0 || measurement.RTT > obrimClockMaxRTT {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockCalculateOffset calculates the local clock offset from an NTP response.
|
||||||
|
func obrimClockCalculateOffset(measurement *obrimClockMeasurement) int {
|
||||||
|
if measurement == nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
midpoint := measurement.RequestTime.Add(measurement.RTT / 2)
|
||||||
|
measurement.Offset = measurement.ServerTime.Sub(midpoint)
|
||||||
|
|
||||||
|
return obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockValidateMeasurement validates an NTP synchronization measurement.
|
||||||
|
func obrimClockValidateMeasurement(measurement *obrimClockMeasurement) int {
|
||||||
|
if measurement == nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if measurement.RTT <= 0 || measurement.RTT > obrimClockMaxRTT {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if measurement.ServerTime.IsZero() {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if measurement.RequestTime.IsZero() || measurement.ResponseTime.IsZero() {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if measurement.ResponseTime.Before(measurement.RequestTime) {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
measurement.Valid = true
|
||||||
|
|
||||||
|
return obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockCalculateAverageOffset calculates the trusted average clock offset.
|
||||||
|
func obrimClockCalculateAverageOffset(measurements []obrimClockMeasurement) (time.Duration, int) {
|
||||||
|
var total int64
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
for _, measurement := range measurements {
|
||||||
|
if !measurement.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
total += measurement.Offset.Nanoseconds()
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return 0, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Duration(total / count), obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockUpdateClock updates the runtime clock state.
|
||||||
|
func obrimClockUpdateClock(clockOffset int64, lastSync int64) {
|
||||||
|
obrimClockMutex.Lock()
|
||||||
|
defer obrimClockMutex.Unlock()
|
||||||
|
|
||||||
|
obrimClockOffset = clockOffset
|
||||||
|
obrimClockLastSync = lastSync
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockPersistClock writes the runtime clock state to persistent storage.
|
||||||
|
func obrimClockPersistClock() int {
|
||||||
|
obrimClockMutex.RLock()
|
||||||
|
|
||||||
|
state := obrimClockState{
|
||||||
|
ClockOffset: obrimClockOffset,
|
||||||
|
LastSync: obrimClockLastSync,
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimClockMutex.RUnlock()
|
||||||
|
|
||||||
|
configPath, err := obrimClockConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
var document obrimClockFile
|
||||||
|
|
||||||
|
if len(data) != 0 {
|
||||||
|
if err := json.Unmarshal(data, &document); err != nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.Config == nil {
|
||||||
|
document.Config = &obrimClockConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.Config.Clock = &state
|
||||||
|
|
||||||
|
updatedData, err := json.MarshalIndent(document, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
|
||||||
|
return obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockCurrentClock returns the current synchronized UTC.
|
||||||
|
func obrimClockCurrentClock() time.Time {
|
||||||
|
obrimClockMutex.RLock()
|
||||||
|
offset := obrimClockOffset
|
||||||
|
obrimClockMutex.RUnlock()
|
||||||
|
|
||||||
|
return time.Now().UTC().Add(time.Duration(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockLoadClock loads the persisted clock state.
|
||||||
|
func obrimClockLoadClock() (*obrimClockState, int) {
|
||||||
|
configPath, err := obrimClockConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
var document obrimClockFile
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &document); err != nil {
|
||||||
|
return nil, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.Config == nil || document.Config.Clock == nil {
|
||||||
|
return nil, obrimClockFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.Config.Clock, obrimClockSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockConfigPath returns the platform-specific persistent configuration path.
|
||||||
|
func obrimClockConfigPath() (string, error) {
|
||||||
|
var basePath string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
basePath = os.Getenv("APPDATA")
|
||||||
|
|
||||||
|
if basePath == "" {
|
||||||
|
return "", errors.New("APPDATA is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
homePath, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath = filepath.Join(homePath, "Library", "Application Support")
|
||||||
|
|
||||||
|
default:
|
||||||
|
homePath, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath = filepath.Join(homePath, ".config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(
|
||||||
|
basePath,
|
||||||
|
obrimClockSoftwareName,
|
||||||
|
"persistent",
|
||||||
|
"config",
|
||||||
|
"config.json",
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimClockNTPToTime converts an NTP timestamp into Unix UTC time.
|
||||||
|
func obrimClockNTPToTime(timestamp uint64) (time.Time, error) {
|
||||||
|
seconds := timestamp >> 32
|
||||||
|
fraction := timestamp & 0xffffffff
|
||||||
|
|
||||||
|
if seconds < obrimClockNTPUnixOffset {
|
||||||
|
return time.Time{}, errors.New("invalid NTP timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
unixSeconds := int64(seconds) - obrimClockNTPUnixOffset
|
||||||
|
nanoseconds := int64((fraction * 1_000_000_000) >> 32)
|
||||||
|
|
||||||
|
return time.Unix(unixSeconds, nanoseconds).UTC(), nil
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Manager
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provides the clock synchronization service for maintaining a
|
||||||
|
| consistent system time reference across automation tasks.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/module/essential/hidden/service/worker/clock"
|
||||||
|
// Import more workers as needed.
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObrimWorkerManagerAction defines a worker manager action.
|
||||||
|
type ObrimWorkerManagerAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ObrimWorkerManagerActionStart starts all framework workers.
|
||||||
|
ObrimWorkerManagerActionStart ObrimWorkerManagerAction = "start"
|
||||||
|
|
||||||
|
// ObrimWorkerManagerActionStop stops all framework workers.
|
||||||
|
ObrimWorkerManagerActionStop ObrimWorkerManagerAction = "stop"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObrimWorkerManager starts or stops all framework workers.
|
||||||
|
func ObrimWorkerManager(action ObrimWorkerManagerAction) {
|
||||||
|
switch action {
|
||||||
|
case ObrimWorkerManagerActionStart:
|
||||||
|
clock.ObrimClockStart()
|
||||||
|
// Add more worker start functions as needed.
|
||||||
|
|
||||||
|
case ObrimWorkerManagerActionStop:
|
||||||
|
clock.ObrimClockStop()
|
||||||
|
// Add more worker stop functions as needed.
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,641 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Cipher
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provide reusable AES-256 encryption and decryption for file content.
|
||||||
|
| - Support standard and salted encryption formats through a unified interface.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use encrypt-standard to encrypt file content using AES-256 without salt.
|
||||||
|
| - Use encrypt-salted to encrypt file content using AES-256 with a cryptographically secure generated salt.
|
||||||
|
| - Use decrypt-standard to decrypt AES-256 encrypted file content without salt metadata.
|
||||||
|
| - Use decrypt-salted to decrypt self-contained encrypted files with embedded signature, version, salt, and ciphertext metadata.
|
||||||
|
| - Supply the AES-256 compatible key, input file path, and output file path through the configuration.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimCipher("encrypt-standard", map[string]any{
|
||||||
|
| "key": key,
|
||||||
|
| "input": inputPath,
|
||||||
|
| "output": outputPath,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimCipher("encrypt-salted", map[string]any{
|
||||||
|
| "key": key,
|
||||||
|
| "input": inputPath,
|
||||||
|
| "output": outputPath,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimCipher("decrypt-standard", map[string]any{
|
||||||
|
| "key": key,
|
||||||
|
| "input": inputPath,
|
||||||
|
| "output": outputPath,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimCipher("decrypt-salted", map[string]any{
|
||||||
|
| "key": key,
|
||||||
|
| "input": inputPath,
|
||||||
|
| "output": outputPath,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cipher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility execution type identifiers.
|
||||||
|
const (
|
||||||
|
obrimCipherTypeEncryptStandard = "encrypt-standard"
|
||||||
|
obrimCipherTypeEncryptSalted = "encrypt-salted"
|
||||||
|
obrimCipherTypeDecryptStandard = "decrypt-standard"
|
||||||
|
obrimCipherTypeDecryptSalted = "decrypt-salted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cipher format metadata values.
|
||||||
|
const (
|
||||||
|
obrimCipherAlgorithm = "AES-256-GCM"
|
||||||
|
obrimCipherFormat = "OBRIM-CIPHER"
|
||||||
|
obrimCipherVersion = "1"
|
||||||
|
obrimCipherSaltSize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility execution success result codes.
|
||||||
|
const (
|
||||||
|
obrimCipherSuccessEncryptStandard = "SUCCESS_ENCRYPT_STANDARD"
|
||||||
|
obrimCipherSuccessEncryptSalted = "SUCCESS_ENCRYPT_SALTED"
|
||||||
|
obrimCipherSuccessDecryptStandard = "SUCCESS_DECRYPT_STANDARD"
|
||||||
|
obrimCipherSuccessDecryptSalted = "SUCCESS_DECRYPT_SALTED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility execution failure result codes.
|
||||||
|
const (
|
||||||
|
obrimCipherFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimCipherFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
obrimCipherFailureMissingKey = "FAILURE_MISSING_KEY"
|
||||||
|
obrimCipherFailureInvalidKey = "FAILURE_INVALID_KEY"
|
||||||
|
obrimCipherFailureMissingInput = "FAILURE_MISSING_INPUT"
|
||||||
|
obrimCipherFailureInvalidInput = "FAILURE_INVALID_INPUT"
|
||||||
|
obrimCipherFailureMissingOutput = "FAILURE_MISSING_OUTPUT"
|
||||||
|
obrimCipherFailureInvalidOutput = "FAILURE_INVALID_OUTPUT"
|
||||||
|
obrimCipherFailureExternalSalt = "FAILURE_EXTERNAL_SALT"
|
||||||
|
obrimCipherFailureReadInput = "FAILURE_READ_INPUT"
|
||||||
|
obrimCipherFailureWriteOutput = "FAILURE_WRITE_OUTPUT"
|
||||||
|
obrimCipherFailureCipherInitialization = "FAILURE_CIPHER_INITIALIZATION"
|
||||||
|
obrimCipherFailureNonceGeneration = "FAILURE_NONCE_GENERATION"
|
||||||
|
obrimCipherFailureEncryption = "FAILURE_ENCRYPTION"
|
||||||
|
obrimCipherFailureDecryption = "FAILURE_DECRYPTION"
|
||||||
|
obrimCipherFailureSaltGeneration = "FAILURE_SALT_GENERATION"
|
||||||
|
obrimCipherFailureHeaderBuild = "FAILURE_HEADER_BUILD"
|
||||||
|
obrimCipherFailureSignature = "FAILURE_INVALID_SIGNATURE"
|
||||||
|
obrimCipherFailureVersion = "FAILURE_INVALID_VERSION"
|
||||||
|
obrimCipherFailureSaltExtraction = "FAILURE_SALT_EXTRACTION"
|
||||||
|
obrimCipherFailurePayloadExtraction = "FAILURE_PAYLOAD_EXTRACTION"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Standardized utility output.
|
||||||
|
type obrimCipherOutput struct {
|
||||||
|
Status bool
|
||||||
|
Code string
|
||||||
|
Payload any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salted encrypted file header.
|
||||||
|
type obrimCipherHeader struct {
|
||||||
|
Signature []byte
|
||||||
|
Version []byte
|
||||||
|
Salt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standardized operation payload.
|
||||||
|
type obrimCipherPayload struct {
|
||||||
|
Operation string
|
||||||
|
Algorithm string
|
||||||
|
Format string
|
||||||
|
Version string
|
||||||
|
InputPath string
|
||||||
|
OutputPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimCipher executes encryption or decryption operations through a unified interface.
|
||||||
|
func ObrimCipher(typeName string, config map[string]any) map[string]any {
|
||||||
|
if code := obrimCipherValidateInput(typeName, config); code != "" {
|
||||||
|
return obrimCipherBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, code := obrimCipherRouteRequest(typeName, config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimCipherBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimCipherBuildOutput(true, code, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherValidateInput validates the requested operation and its configuration.
|
||||||
|
func obrimCipherValidateInput(typeName string, config map[string]any) string {
|
||||||
|
switch typeName {
|
||||||
|
case obrimCipherTypeEncryptStandard,
|
||||||
|
obrimCipherTypeEncryptSalted,
|
||||||
|
obrimCipherTypeDecryptStandard,
|
||||||
|
obrimCipherTypeDecryptSalted:
|
||||||
|
default:
|
||||||
|
return obrimCipherFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
if config == nil {
|
||||||
|
return obrimCipherFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := config["key"]
|
||||||
|
if !ok || key == nil {
|
||||||
|
return obrimCipherFailureMissingKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if !obrimCipherValidKey(key) {
|
||||||
|
return obrimCipherFailureInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
input, ok := config["input"]
|
||||||
|
if !ok || input == nil {
|
||||||
|
return obrimCipherFailureMissingInput
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, ok := input.(string)
|
||||||
|
if !ok || inputPath == "" {
|
||||||
|
return obrimCipherFailureInvalidInput
|
||||||
|
}
|
||||||
|
|
||||||
|
output, ok := config["output"]
|
||||||
|
if !ok || output == nil {
|
||||||
|
return obrimCipherFailureMissingOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath, ok := output.(string)
|
||||||
|
if !ok || outputPath == "" {
|
||||||
|
return obrimCipherFailureInvalidOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typeName {
|
||||||
|
case obrimCipherTypeEncryptSalted, obrimCipherTypeDecryptSalted:
|
||||||
|
if _, exists := config["salt"]; exists {
|
||||||
|
return obrimCipherFailureExternalSalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherRouteRequest routes a validated request to its operation-specific implementation.
|
||||||
|
func obrimCipherRouteRequest(typeName string, config map[string]any) (map[string]any, string) {
|
||||||
|
switch typeName {
|
||||||
|
case obrimCipherTypeEncryptStandard:
|
||||||
|
return obrimCipherEncryptStandard(config)
|
||||||
|
case obrimCipherTypeEncryptSalted:
|
||||||
|
return obrimCipherEncryptSalted(config)
|
||||||
|
case obrimCipherTypeDecryptStandard:
|
||||||
|
return obrimCipherDecryptStandard(config)
|
||||||
|
case obrimCipherTypeDecryptSalted:
|
||||||
|
return obrimCipherDecryptSalted(config)
|
||||||
|
default:
|
||||||
|
return nil, obrimCipherFailureInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherBuildOutput builds the standardized utility output.
|
||||||
|
func obrimCipherBuildOutput(status bool, code string, payload map[string]any) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"code": code,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherEncryptStandard encrypts file content without salt metadata.
|
||||||
|
func obrimCipherEncryptStandard(config map[string]any) (map[string]any, string) {
|
||||||
|
key, ok := obrimCipherKey(config["key"])
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, outputPath, ok := obrimCipherPaths(config)
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := os.ReadFile(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureReadInput
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, code := obrimCipherEncrypt(key, plaintext, nil)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPath, ciphertext, 0600); err != nil {
|
||||||
|
return nil, obrimCipherFailureWriteOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"operation": obrimCipherTypeEncryptStandard,
|
||||||
|
"algorithm": obrimCipherAlgorithm,
|
||||||
|
"inputPath": inputPath,
|
||||||
|
"outputPath": outputPath,
|
||||||
|
}, obrimCipherSuccessEncryptStandard
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherGenerateSalt generates a cryptographically secure salt.
|
||||||
|
func obrimCipherGenerateSalt() ([]byte, string) {
|
||||||
|
salt := make([]byte, obrimCipherSaltSize)
|
||||||
|
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return nil, obrimCipherFailureSaltGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
return salt, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherBuildHeader builds the salted cipher signature, version, and salt header.
|
||||||
|
func obrimCipherBuildHeader(salt []byte) ([]byte, string) {
|
||||||
|
if len(salt) != obrimCipherSaltSize {
|
||||||
|
return nil, obrimCipherFailureHeaderBuild
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := []byte(obrimCipherFormat)
|
||||||
|
version := []byte(obrimCipherVersion)
|
||||||
|
|
||||||
|
header := make([]byte, 0, len(signature)+len(version)+len(salt))
|
||||||
|
header = append(header, signature...)
|
||||||
|
header = append(header, version...)
|
||||||
|
header = append(header, salt...)
|
||||||
|
|
||||||
|
return header, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherEncryptSalted encrypts file content with embedded salt metadata.
|
||||||
|
func obrimCipherEncryptSalted(config map[string]any) (map[string]any, string) {
|
||||||
|
key, ok := obrimCipherKey(config["key"])
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, outputPath, ok := obrimCipherPaths(config)
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := os.ReadFile(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureReadInput
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, code := obrimCipherGenerateSalt()
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
header, code := obrimCipherBuildHeader(salt)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, code := obrimCipherEncrypt(key, plaintext, salt)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
output := make([]byte, 0, len(header)+len(ciphertext))
|
||||||
|
output = append(output, header...)
|
||||||
|
output = append(output, ciphertext...)
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPath, output, 0600); err != nil {
|
||||||
|
return nil, obrimCipherFailureWriteOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"operation": obrimCipherTypeEncryptSalted,
|
||||||
|
"algorithm": obrimCipherAlgorithm,
|
||||||
|
"format": obrimCipherFormat,
|
||||||
|
"version": obrimCipherVersion,
|
||||||
|
"inputPath": inputPath,
|
||||||
|
"outputPath": outputPath,
|
||||||
|
}, obrimCipherSuccessEncryptSalted
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherDecryptStandard decrypts file content without salt metadata.
|
||||||
|
func obrimCipherDecryptStandard(config map[string]any) (map[string]any, string) {
|
||||||
|
key, ok := obrimCipherKey(config["key"])
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, outputPath, ok := obrimCipherPaths(config)
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := os.ReadFile(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureReadInput
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, code := obrimCipherDecrypt(key, ciphertext, nil)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPath, plaintext, 0600); err != nil {
|
||||||
|
return nil, obrimCipherFailureWriteOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"operation": obrimCipherTypeDecryptStandard,
|
||||||
|
"algorithm": obrimCipherAlgorithm,
|
||||||
|
"inputPath": inputPath,
|
||||||
|
"outputPath": outputPath,
|
||||||
|
}, obrimCipherSuccessDecryptStandard
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherParseHeader parses the signature and version metadata.
|
||||||
|
func obrimCipherParseHeader(data []byte) (obrimCipherHeader, string) {
|
||||||
|
signatureLength := len(obrimCipherFormat)
|
||||||
|
versionLength := len(obrimCipherVersion)
|
||||||
|
|
||||||
|
minimumLength := signatureLength + versionLength + obrimCipherSaltSize
|
||||||
|
if len(data) < minimumLength {
|
||||||
|
return obrimCipherHeader{}, obrimCipherFailureSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := data[:signatureLength]
|
||||||
|
if string(signature) != obrimCipherFormat {
|
||||||
|
return obrimCipherHeader{}, obrimCipherFailureSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStart := signatureLength
|
||||||
|
versionEnd := versionStart + versionLength
|
||||||
|
version := data[versionStart:versionEnd]
|
||||||
|
|
||||||
|
if string(version) != obrimCipherVersion {
|
||||||
|
return obrimCipherHeader{}, obrimCipherFailureVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
saltStart := versionEnd
|
||||||
|
saltEnd := saltStart + obrimCipherSaltSize
|
||||||
|
|
||||||
|
return obrimCipherHeader{
|
||||||
|
Signature: append([]byte(nil), signature...),
|
||||||
|
Version: append([]byte(nil), version...),
|
||||||
|
Salt: append([]byte(nil), data[saltStart:saltEnd]...),
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherExtractSalt extracts the embedded salt from a parsed salted file.
|
||||||
|
func obrimCipherExtractSalt(data []byte, header obrimCipherHeader) ([]byte, string) {
|
||||||
|
if len(header.Salt) != obrimCipherSaltSize {
|
||||||
|
return nil, obrimCipherFailureSaltExtraction
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureLength := len(header.Signature)
|
||||||
|
versionLength := len(header.Version)
|
||||||
|
saltStart := signatureLength + versionLength
|
||||||
|
saltEnd := saltStart + obrimCipherSaltSize
|
||||||
|
|
||||||
|
if saltStart < 0 || saltEnd > len(data) {
|
||||||
|
return nil, obrimCipherFailureSaltExtraction
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]byte(nil), data[saltStart:saltEnd]...), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherExtractPayload extracts the encrypted payload after the salted header.
|
||||||
|
func obrimCipherExtractPayload(data []byte, header obrimCipherHeader) ([]byte, string) {
|
||||||
|
payloadStart := len(header.Signature) + len(header.Version) + len(header.Salt)
|
||||||
|
|
||||||
|
if payloadStart >= len(data) {
|
||||||
|
return nil, obrimCipherFailurePayloadExtraction
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := data[payloadStart:]
|
||||||
|
if len(payload) <= 0 {
|
||||||
|
return nil, obrimCipherFailurePayloadExtraction
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]byte(nil), payload...), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherDecryptSalted decrypts file content using its embedded salt metadata.
|
||||||
|
func obrimCipherDecryptSalted(config map[string]any) (map[string]any, string) {
|
||||||
|
key, ok := obrimCipherKey(config["key"])
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, outputPath, ok := obrimCipherPaths(config)
|
||||||
|
if !ok {
|
||||||
|
return nil, obrimCipherFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureReadInput
|
||||||
|
}
|
||||||
|
|
||||||
|
header, code := obrimCipherParseHeader(data)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, code := obrimCipherExtractSalt(data, header)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, code := obrimCipherExtractPayload(data, header)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, code := obrimCipherDecrypt(key, payload, salt)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputPath, plaintext, 0600); err != nil {
|
||||||
|
return nil, obrimCipherFailureWriteOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"operation": obrimCipherTypeDecryptSalted,
|
||||||
|
"algorithm": obrimCipherAlgorithm,
|
||||||
|
"format": string(header.Signature),
|
||||||
|
"version": string(header.Version),
|
||||||
|
"inputPath": inputPath,
|
||||||
|
"outputPath": outputPath,
|
||||||
|
}, obrimCipherSuccessDecryptSalted
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherEncrypt performs AES-256-GCM encryption and prefixes the nonce to the ciphertext.
|
||||||
|
func obrimCipherEncrypt(key []byte, plaintext []byte, salt []byte) ([]byte, string) {
|
||||||
|
derivedKey := obrimCipherDeriveKey(key, salt)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureCipherInitialization
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureCipherInitialization
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, aead.NonceSize())
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, obrimCipherFailureNonceGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := aead.Seal(nil, nonce, plaintext, nil)
|
||||||
|
|
||||||
|
output := make([]byte, 0, len(nonce)+len(ciphertext))
|
||||||
|
output = append(output, nonce...)
|
||||||
|
output = append(output, ciphertext...)
|
||||||
|
|
||||||
|
return output, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherDecrypt performs AES-256-GCM decryption after extracting the nonce.
|
||||||
|
func obrimCipherDecrypt(key []byte, ciphertext []byte, salt []byte) ([]byte, string) {
|
||||||
|
derivedKey := obrimCipherDeriveKey(key, salt)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(derivedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureCipherInitialization
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureCipherInitialization
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := aead.NonceSize()
|
||||||
|
if len(ciphertext) <= nonceSize {
|
||||||
|
return nil, obrimCipherFailureDecryption
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := ciphertext[:nonceSize]
|
||||||
|
payload := ciphertext[nonceSize:]
|
||||||
|
|
||||||
|
plaintext, err := aead.Open(nil, nonce, payload, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCipherFailureDecryption
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherDeriveKey derives the AES-256 key from the supplied key and optional salt.
|
||||||
|
func obrimCipherDeriveKey(key []byte, salt []byte) []byte {
|
||||||
|
if len(salt) == 0 {
|
||||||
|
return append([]byte(nil), key...)
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.New()
|
||||||
|
_, _ = digest.Write(key)
|
||||||
|
_, _ = digest.Write(salt)
|
||||||
|
|
||||||
|
return digest.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherKey normalizes a supported AES-256 key value.
|
||||||
|
func obrimCipherKey(value any) ([]byte, bool) {
|
||||||
|
switch key := value.(type) {
|
||||||
|
case string:
|
||||||
|
if len([]byte(key)) != 32 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(key), true
|
||||||
|
case []byte:
|
||||||
|
if len(key) != 32 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]byte(nil), key...), true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherValidKey validates that a supplied key is AES-256 compatible.
|
||||||
|
func obrimCipherValidKey(value any) bool {
|
||||||
|
_, ok := obrimCipherKey(value)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherPaths resolves and validates the configured input and output paths.
|
||||||
|
func obrimCipherPaths(config map[string]any) (string, string, bool) {
|
||||||
|
input, inputOK := config["input"].(string)
|
||||||
|
output, outputOK := config["output"].(string)
|
||||||
|
|
||||||
|
if !inputOK || !outputOK || input == "" || output == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath, err := filepath.Abs(input)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath, err := filepath.Abs(output)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if inputPath == outputPath {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(inputPath); err != nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputPath, outputPath, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimCipherError provides a deterministic error representation for internal callers.
|
||||||
|
func obrimCipherError(code string) error {
|
||||||
|
return errors.New(fmt.Sprintf("cipher: %s", code))
|
||||||
|
}
|
||||||
@ -0,0 +1,689 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Codec
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provide unified encoding and decoding capabilities using Base8,
|
||||||
|
| Base10, Base16, Base32, Base64, and Sqids with centralized
|
||||||
|
| validation, operation dispatching, transformation handling,
|
||||||
|
| and standardized result generation.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use the utility to encode input data using the selected codec format.
|
||||||
|
| - Use the utility to decode input data using the selected codec format.
|
||||||
|
| - Configure the codec format using base8, base10, base16, base32,
|
||||||
|
| base64, or sqids.
|
||||||
|
| - Provide input data as a string or []byte.
|
||||||
|
| - Configure charset and salt transformations when required.
|
||||||
|
| - Configure lower or upper output casing when required.
|
||||||
|
| - Configure minimum output length for Sqids encoding.
|
||||||
|
| - Configure length for deterministic Sqids decoding.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimCodec("encode", map[string]any{
|
||||||
|
| "format": "base64",
|
||||||
|
| "data": "hello",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimCodec("decode", map[string]any{
|
||||||
|
| "format": "base64",
|
||||||
|
| "data": "aGVsbG8=",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimCodec("encode", map[string]any{
|
||||||
|
| "format": "sqids",
|
||||||
|
| "data": "12345",
|
||||||
|
| "charset": "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
| "salt": "example",
|
||||||
|
| "length": 8,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sqids/sqids-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimCodecTypeEncode = "encode"
|
||||||
|
obrimCodecTypeDecode = "decode"
|
||||||
|
|
||||||
|
obrimCodecFormatBase8 = "base8"
|
||||||
|
obrimCodecFormatBase10 = "base10"
|
||||||
|
obrimCodecFormatBase16 = "base16"
|
||||||
|
obrimCodecFormatBase32 = "base32"
|
||||||
|
obrimCodecFormatBase64 = "base64"
|
||||||
|
obrimCodecFormatSqids = "sqids"
|
||||||
|
|
||||||
|
obrimCodecCaseLower = "lower"
|
||||||
|
obrimCodecCaseUpper = "upper"
|
||||||
|
|
||||||
|
obrimCodecSuccessEncoded = "SUCCESS_ENCODED"
|
||||||
|
obrimCodecSuccessDecoded = "SUCCESS_DECODED"
|
||||||
|
|
||||||
|
obrimCodecFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimCodecFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
obrimCodecFailureInvalidFormat = "FAILURE_INVALID_FORMAT"
|
||||||
|
obrimCodecFailureInvalidData = "FAILURE_INVALID_DATA"
|
||||||
|
obrimCodecFailureInvalidCharset = "FAILURE_INVALID_CHARSET"
|
||||||
|
obrimCodecFailureInvalidSalt = "FAILURE_INVALID_SALT"
|
||||||
|
obrimCodecFailureInvalidCase = "FAILURE_INVALID_CASE"
|
||||||
|
obrimCodecFailureInvalidLength = "FAILURE_INVALID_LENGTH"
|
||||||
|
obrimCodecFailureEncoding = "FAILURE_ENCODING"
|
||||||
|
obrimCodecFailureDecoding = "FAILURE_DECODING"
|
||||||
|
obrimCodecFailureTransformation = "FAILURE_TRANSFORMATION"
|
||||||
|
obrimCodecFailureTransformationReversal = "FAILURE_TRANSFORMATION_REVERSAL"
|
||||||
|
obrimCodecFailureUnsupportedData = "FAILURE_UNSUPPORTED_DATA"
|
||||||
|
obrimCodecFailureSqids = "FAILURE_SQIDS"
|
||||||
|
)
|
||||||
|
|
||||||
|
type obrimCodecConfig struct {
|
||||||
|
format string
|
||||||
|
data []byte
|
||||||
|
charset string
|
||||||
|
salt string
|
||||||
|
caseRule string
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimCodecResult struct {
|
||||||
|
operation string
|
||||||
|
format string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimCodecOutput struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload *obrimCodecResult `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObrimCodec(typ string, config map[string]any) map[string]any {
|
||||||
|
normalized, code := obrimCodecValidateInput(typ, config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimCodecBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, code := obrimCodecRouteRequest(typ, normalized)
|
||||||
|
if code != "" {
|
||||||
|
return obrimCodecBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ == obrimCodecTypeEncode {
|
||||||
|
return obrimCodecBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimCodecSuccessEncoded,
|
||||||
|
&obrimCodecResult{
|
||||||
|
operation: typ,
|
||||||
|
format: normalized.format,
|
||||||
|
value: value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimCodecBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimCodecSuccessDecoded,
|
||||||
|
&obrimCodecResult{
|
||||||
|
operation: typ,
|
||||||
|
format: normalized.format,
|
||||||
|
value: value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecValidateInput(typ string, config map[string]any) (obrimCodecConfig, string) {
|
||||||
|
var result obrimCodecConfig
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case obrimCodecTypeEncode, obrimCodecTypeDecode:
|
||||||
|
default:
|
||||||
|
return result, obrimCodecFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValue, exists := config["format"]
|
||||||
|
if !exists {
|
||||||
|
return result, obrimCodecFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
format, ok := formatValue.(string)
|
||||||
|
if !ok || format == "" {
|
||||||
|
return result, obrimCodecFailureInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case obrimCodecFormatBase8,
|
||||||
|
obrimCodecFormatBase10,
|
||||||
|
obrimCodecFormatBase16,
|
||||||
|
obrimCodecFormatBase32,
|
||||||
|
obrimCodecFormatBase64,
|
||||||
|
obrimCodecFormatSqids:
|
||||||
|
default:
|
||||||
|
return result, obrimCodecFailureInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
dataValue, exists := config["data"]
|
||||||
|
if !exists {
|
||||||
|
return result, obrimCodecFailureInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := dataValue.(type) {
|
||||||
|
case string:
|
||||||
|
if value == "" {
|
||||||
|
return result, obrimCodecFailureInvalidData
|
||||||
|
}
|
||||||
|
result.data = []byte(value)
|
||||||
|
case []byte:
|
||||||
|
if len(value) == 0 {
|
||||||
|
return result, obrimCodecFailureInvalidData
|
||||||
|
}
|
||||||
|
result.data = append([]byte(nil), value...)
|
||||||
|
default:
|
||||||
|
return result, obrimCodecFailureUnsupportedData
|
||||||
|
}
|
||||||
|
|
||||||
|
result.format = format
|
||||||
|
|
||||||
|
if charsetValue, exists := config["charset"]; exists {
|
||||||
|
charset, ok := charsetValue.(string)
|
||||||
|
if !ok || charset == "" {
|
||||||
|
return result, obrimCodecFailureInvalidCharset
|
||||||
|
}
|
||||||
|
result.charset = charset
|
||||||
|
}
|
||||||
|
|
||||||
|
if saltValue, exists := config["salt"]; exists {
|
||||||
|
salt, ok := saltValue.(string)
|
||||||
|
if !ok || salt == "" {
|
||||||
|
return result, obrimCodecFailureInvalidSalt
|
||||||
|
}
|
||||||
|
result.salt = salt
|
||||||
|
}
|
||||||
|
|
||||||
|
if caseValue, exists := config["case"]; exists {
|
||||||
|
caseRule, ok := caseValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return result, obrimCodecFailureInvalidCase
|
||||||
|
}
|
||||||
|
|
||||||
|
switch caseRule {
|
||||||
|
case obrimCodecCaseLower, obrimCodecCaseUpper:
|
||||||
|
result.caseRule = caseRule
|
||||||
|
default:
|
||||||
|
return result, obrimCodecFailureInvalidCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lengthValue, exists := config["length"]; exists {
|
||||||
|
switch length := lengthValue.(type) {
|
||||||
|
case int:
|
||||||
|
result.length = length
|
||||||
|
case int8:
|
||||||
|
result.length = int(length)
|
||||||
|
case int16:
|
||||||
|
result.length = int(length)
|
||||||
|
case int32:
|
||||||
|
result.length = int(length)
|
||||||
|
case int64:
|
||||||
|
result.length = int(length)
|
||||||
|
case uint:
|
||||||
|
result.length = int(length)
|
||||||
|
case uint8:
|
||||||
|
result.length = int(length)
|
||||||
|
case uint16:
|
||||||
|
result.length = int(length)
|
||||||
|
case uint32:
|
||||||
|
result.length = int(length)
|
||||||
|
case uint64:
|
||||||
|
if uint64(int(length)) != length {
|
||||||
|
return result, obrimCodecFailureInvalidLength
|
||||||
|
}
|
||||||
|
result.length = int(length)
|
||||||
|
case float64:
|
||||||
|
if length != float64(int(length)) {
|
||||||
|
return result, obrimCodecFailureInvalidLength
|
||||||
|
}
|
||||||
|
result.length = int(length)
|
||||||
|
default:
|
||||||
|
return result, obrimCodecFailureInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.length < 0 {
|
||||||
|
return result, obrimCodecFailureInvalidLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == obrimCodecFormatSqids && result.length == 0 {
|
||||||
|
result.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecRouteRequest(typ string, config obrimCodecConfig) (string, string) {
|
||||||
|
switch typ {
|
||||||
|
case obrimCodecTypeEncode:
|
||||||
|
return obrimCodecEncode(config)
|
||||||
|
case obrimCodecTypeDecode:
|
||||||
|
return obrimCodecDecode(config)
|
||||||
|
default:
|
||||||
|
return "", obrimCodecFailureInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecBuildOutput(status bool, code string, payload *obrimCodecResult) map[string]any {
|
||||||
|
if !status {
|
||||||
|
return map[string]any{
|
||||||
|
"status": false,
|
||||||
|
"code": code,
|
||||||
|
"payload": nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"status": true,
|
||||||
|
"code": code,
|
||||||
|
"payload": map[string]any{
|
||||||
|
"operation": payload.operation,
|
||||||
|
"format": payload.format,
|
||||||
|
"value": payload.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncode(config obrimCodecConfig) (string, string) {
|
||||||
|
data, code := obrimCodecApplyEncodeTransformations(config.data, config)
|
||||||
|
if code != "" {
|
||||||
|
return "", code
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
value string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch config.format {
|
||||||
|
case obrimCodecFormatBase8:
|
||||||
|
value, err = obrimCodecEncodeBase8(data)
|
||||||
|
case obrimCodecFormatBase10:
|
||||||
|
value, err = obrimCodecEncodeBase10(data)
|
||||||
|
case obrimCodecFormatBase16:
|
||||||
|
value, err = obrimCodecEncodeBase16(data)
|
||||||
|
case obrimCodecFormatBase32:
|
||||||
|
value, err = obrimCodecEncodeBase32(data)
|
||||||
|
case obrimCodecFormatBase64:
|
||||||
|
value, err = obrimCodecEncodeBase64(data)
|
||||||
|
case obrimCodecFormatSqids:
|
||||||
|
value, err = obrimCodecEncodeSqids(data, config)
|
||||||
|
default:
|
||||||
|
return "", obrimCodecFailureInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if config.format == obrimCodecFormatSqids {
|
||||||
|
return "", obrimCodecFailureSqids
|
||||||
|
}
|
||||||
|
return "", obrimCodecFailureEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
value = obrimCodecApplyCase(value, config.caseRule)
|
||||||
|
return value, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecode(config obrimCodecConfig) (string, string) {
|
||||||
|
data := append([]byte(nil), config.data...)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var value []byte
|
||||||
|
|
||||||
|
switch config.format {
|
||||||
|
case obrimCodecFormatBase8:
|
||||||
|
value, err = obrimCodecDecodeBase8(string(data))
|
||||||
|
case obrimCodecFormatBase10:
|
||||||
|
value, err = obrimCodecDecodeBase10(string(data))
|
||||||
|
case obrimCodecFormatBase16:
|
||||||
|
value, err = obrimCodecDecodeBase16(string(data))
|
||||||
|
case obrimCodecFormatBase32:
|
||||||
|
value, err = obrimCodecDecodeBase32(string(data))
|
||||||
|
case obrimCodecFormatBase64:
|
||||||
|
value, err = obrimCodecDecodeBase64(string(data))
|
||||||
|
case obrimCodecFormatSqids:
|
||||||
|
value, err = obrimCodecDecodeSqids(string(data), config)
|
||||||
|
default:
|
||||||
|
return "", obrimCodecFailureInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if config.format == obrimCodecFormatSqids {
|
||||||
|
return "", obrimCodecFailureSqids
|
||||||
|
}
|
||||||
|
return "", obrimCodecFailureDecoding
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err = obrimCodecApplyDecodeTransformations(value, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", obrimCodecFailureTransformationReversal
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(value), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeBase8(data []byte) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int).SetBytes(data)
|
||||||
|
return number.Text(8), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeBase10(data []byte) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int).SetBytes(data)
|
||||||
|
return number.Text(10), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeBase16(data []byte) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeBase32(data []byte) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeBase64(data []byte) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecEncodeSqids(data []byte, config obrimCodecConfig) (string, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int).SetBytes(data)
|
||||||
|
|
||||||
|
if !number.IsUint64() {
|
||||||
|
return "", fmt.Errorf("sqids input exceeds uint64")
|
||||||
|
}
|
||||||
|
|
||||||
|
options := sqids.Options{}
|
||||||
|
|
||||||
|
if config.charset != "" {
|
||||||
|
options.Alphabet = config.charset
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.length > 0 {
|
||||||
|
options.MinLength = config.length
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder, err := sqids.New(options)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoder.Encode([]uint64{number.Uint64()}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeBase8(data string) ([]byte, error) {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int)
|
||||||
|
if _, ok := number.SetString(data, 8); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid base8 value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimCodecBigIntBytes(number), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeBase10(data string) ([]byte, error) {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int)
|
||||||
|
if _, ok := number.SetString(data, 10); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid base10 value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimCodecBigIntBytes(number), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeBase16(data string) ([]byte, error) {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data)%2 != 0 {
|
||||||
|
data = "0" + data
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.DecodeString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeBase32(data string) ([]byte, error) {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
padding := len(data) % 8
|
||||||
|
if padding != 0 {
|
||||||
|
data += strings.Repeat("=", 8-padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base32.StdEncoding.DecodeString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeBase64(data string) ([]byte, error) {
|
||||||
|
data = strings.TrimSpace(data)
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.DecodeString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecDecodeSqids(data string, config obrimCodecConfig) ([]byte, error) {
|
||||||
|
if data == "" {
|
||||||
|
return nil, fmt.Errorf("empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
options := sqids.Options{}
|
||||||
|
|
||||||
|
if config.charset != "" {
|
||||||
|
options.Alphabet = config.charset
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.length > 0 {
|
||||||
|
options.MinLength = config.length
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder, err := sqids.New(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
numbers := encoder.Decode(data)
|
||||||
|
if len(numbers) != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid sqids value")
|
||||||
|
}
|
||||||
|
|
||||||
|
number := new(big.Int).SetUint64(numbers[0])
|
||||||
|
return obrimCodecBigIntBytes(number), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecBigIntBytes(number *big.Int) []byte {
|
||||||
|
if number.Sign() == 0 {
|
||||||
|
return []byte{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
return number.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplyEncodeTransformations(data []byte, config obrimCodecConfig) ([]byte, string) {
|
||||||
|
result := append([]byte(nil), data...)
|
||||||
|
|
||||||
|
if config.salt != "" {
|
||||||
|
result = obrimCodecApplySalt(result, []byte(config.salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.charset != "" && config.format != obrimCodecFormatSqids {
|
||||||
|
transformed, err := obrimCodecApplyCharsetEncode(result, config.charset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimCodecFailureTransformation
|
||||||
|
}
|
||||||
|
result = transformed
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplyDecodeTransformations(data []byte, config obrimCodecConfig) ([]byte, error) {
|
||||||
|
result := append([]byte(nil), data...)
|
||||||
|
|
||||||
|
if config.charset != "" && config.format != obrimCodecFormatSqids {
|
||||||
|
transformed, err := obrimCodecApplyCharsetDecode(result, config.charset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = transformed
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.salt != "" {
|
||||||
|
result = obrimCodecApplySalt(result, []byte(config.salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplyCharsetEncode(data []byte, charset string) ([]byte, error) {
|
||||||
|
charsetBytes := []byte(charset)
|
||||||
|
if len(charsetBytes) < 2 {
|
||||||
|
return nil, fmt.Errorf("charset must contain at least two characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
|
||||||
|
for index, value := range data {
|
||||||
|
result[index] = charsetBytes[int(value)%len(charsetBytes)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplyCharsetDecode(data []byte, charset string) ([]byte, error) {
|
||||||
|
charsetBytes := []byte(charset)
|
||||||
|
if len(charsetBytes) < 2 {
|
||||||
|
return nil, fmt.Errorf("charset must contain at least two characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse := make(map[byte]byte, len(charsetBytes))
|
||||||
|
|
||||||
|
for index, value := range charsetBytes {
|
||||||
|
if _, exists := reverse[value]; exists {
|
||||||
|
return nil, fmt.Errorf("charset contains duplicate characters")
|
||||||
|
}
|
||||||
|
reverse[value] = byte(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
|
||||||
|
for index, value := range data {
|
||||||
|
decoded, exists := reverse[value]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("character not present in charset")
|
||||||
|
}
|
||||||
|
result[index] = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplySalt(data []byte, salt []byte) []byte {
|
||||||
|
if len(salt) == 0 {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]byte, len(data))
|
||||||
|
|
||||||
|
for index, value := range data {
|
||||||
|
result[index] = value ^ salt[index%len(salt)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimCodecApplyCase(value string, caseRule string) string {
|
||||||
|
switch caseRule {
|
||||||
|
case obrimCodecCaseLower:
|
||||||
|
return strings.ToLower(value)
|
||||||
|
case obrimCodecCaseUpper:
|
||||||
|
return strings.ToUpper(value)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = bytes.Compare
|
||||||
|
var _ = json.Valid
|
||||||
@ -0,0 +1,344 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Datetime
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Retrieve the current date and time from the host system clock or
|
||||||
|
| trusted clock state and return the formatted result using a supported
|
||||||
|
| framework date/time pattern.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use local to retrieve the current date and time while preserving the
|
||||||
|
| host system timezone.
|
||||||
|
| - Use cloud to retrieve the current trusted UTC date and time using the
|
||||||
|
| resolved clock state.
|
||||||
|
| - Provide the format configuration when a specific supported date/time
|
||||||
|
| representation is required.
|
||||||
|
| - Use the utility through ObrimDatetime(type string, config map[string]any).
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimDatetime("local", map[string]any{
|
||||||
|
| "format": "yyyy-MM-dd",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimDatetime("local", map[string]any{
|
||||||
|
| "format": "HH:mm:ss",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimDatetime("cloud", map[string]any{
|
||||||
|
| "format": "yyyy-MM-dd HH:mm:ss z",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimDatetime("cloud", map[string]any{
|
||||||
|
| "format": "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package datetime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go/module/essential/hidden/service/worker/clock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reusable datetime input and result constants.
|
||||||
|
const (
|
||||||
|
obrimDatetimeTypeLocal = "local"
|
||||||
|
obrimDatetimeTypeCloud = "cloud"
|
||||||
|
|
||||||
|
obrimDatetimeFormatTimestampSec = "timestampsec"
|
||||||
|
obrimDatetimeFormatTimestampMil = "timestampmil"
|
||||||
|
obrimDatetimeFormatYearMonthDay = "yyyy-MM-dd"
|
||||||
|
obrimDatetimeFormatMonthDayYear = "MMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatFullMonthDayYear = "MMMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatWeekdayMonthDayYear = "EEE, MMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatWeekdayFullMonthDayYear = "EEE, MMMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatFullWeekdayMonthDayYear = "EEEE, MMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatFullWeekdayFullMonthDayYear = "EEEE, MMMM dd, yyyy"
|
||||||
|
obrimDatetimeFormatTime = "HH:mm:ss"
|
||||||
|
obrimDatetimeFormatTime12 = "hh:mm:ss a"
|
||||||
|
obrimDatetimeFormatDateTime = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
obrimDatetimeFormatDateTimeZone = "yyyy-MM-dd HH:mm:ss z"
|
||||||
|
obrimDatetimeFormatISO8601 = "yyyy-MM-dd'T'HH:mm:ssXXX"
|
||||||
|
|
||||||
|
obrimDatetimeSuccessRetrieved = "SUCCESS_DATETIME_RETRIEVED"
|
||||||
|
obrimDatetimeFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimDatetimeFailureInvalidFormat = "FAILURE_INVALID_FORMAT"
|
||||||
|
obrimDatetimeFailureTrustedTimeSource = "FAILURE_TRUSTED_TIME_SOURCE_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Datetime clock state contains the resolved synchronization information.
|
||||||
|
type obrimDatetimeClockState struct {
|
||||||
|
clockOffset int64
|
||||||
|
lastSync int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datetime output contains the standardized utility response.
|
||||||
|
type obrimDatetimeOutput struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload any `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datetime payload contains the formatted datetime result.
|
||||||
|
type obrimDatetimePayload struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimDatetime retrieves and formats the current date and time.
|
||||||
|
func ObrimDatetime(datetimeType string, config map[string]any) map[string]any {
|
||||||
|
format, code := obrimDatetimeValidateInput(datetimeType, config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimDatetimeBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, available := obrimDatetimeResolveState()
|
||||||
|
|
||||||
|
switch datetimeType {
|
||||||
|
case obrimDatetimeTypeLocal:
|
||||||
|
value := obrimDatetimeLocal(format, state, available)
|
||||||
|
return obrimDatetimeBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimDatetimeSuccessRetrieved,
|
||||||
|
&obrimDatetimePayload{Value: value},
|
||||||
|
)
|
||||||
|
|
||||||
|
case obrimDatetimeTypeCloud:
|
||||||
|
if !available {
|
||||||
|
return obrimDatetimeBuildOutput(
|
||||||
|
false,
|
||||||
|
obrimDatetimeFailureTrustedTimeSource,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := obrimDatetimeCloud(format, state)
|
||||||
|
return obrimDatetimeBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimDatetimeSuccessRetrieved,
|
||||||
|
&obrimDatetimePayload{Value: value},
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return obrimDatetimeBuildOutput(false, obrimDatetimeFailureInvalidType, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeValidateInput validates the requested datetime type and format.
|
||||||
|
func obrimDatetimeValidateInput(datetimeType string, config map[string]any) (string, string) {
|
||||||
|
switch datetimeType {
|
||||||
|
case obrimDatetimeTypeLocal, obrimDatetimeTypeCloud:
|
||||||
|
default:
|
||||||
|
return "", obrimDatetimeFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
format := ""
|
||||||
|
if config != nil {
|
||||||
|
if value, exists := config["format"]; exists {
|
||||||
|
var valid bool
|
||||||
|
format, valid = value.(string)
|
||||||
|
if !valid || format == "" {
|
||||||
|
return "", obrimDatetimeFailureInvalidFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "" {
|
||||||
|
format = obrimDatetimeFormatDateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case obrimDatetimeFormatTimestampSec,
|
||||||
|
obrimDatetimeFormatTimestampMil,
|
||||||
|
obrimDatetimeFormatYearMonthDay,
|
||||||
|
obrimDatetimeFormatMonthDayYear,
|
||||||
|
obrimDatetimeFormatFullMonthDayYear,
|
||||||
|
obrimDatetimeFormatWeekdayMonthDayYear,
|
||||||
|
obrimDatetimeFormatWeekdayFullMonthDayYear,
|
||||||
|
obrimDatetimeFormatFullWeekdayMonthDayYear,
|
||||||
|
obrimDatetimeFormatFullWeekdayFullMonthDayYear,
|
||||||
|
obrimDatetimeFormatTime,
|
||||||
|
obrimDatetimeFormatTime12,
|
||||||
|
obrimDatetimeFormatDateTime,
|
||||||
|
obrimDatetimeFormatDateTimeZone,
|
||||||
|
obrimDatetimeFormatISO8601:
|
||||||
|
return format, ""
|
||||||
|
default:
|
||||||
|
return "", obrimDatetimeFailureInvalidFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeRouteRequest routes the request to its type-specific operation.
|
||||||
|
func obrimDatetimeRouteRequest(
|
||||||
|
datetimeType string,
|
||||||
|
format string,
|
||||||
|
state obrimDatetimeClockState,
|
||||||
|
available bool,
|
||||||
|
) (string, string) {
|
||||||
|
switch datetimeType {
|
||||||
|
case obrimDatetimeTypeLocal:
|
||||||
|
return obrimDatetimeLocal(format, state, available), ""
|
||||||
|
case obrimDatetimeTypeCloud:
|
||||||
|
if !available {
|
||||||
|
return "", obrimDatetimeFailureTrustedTimeSource
|
||||||
|
}
|
||||||
|
return obrimDatetimeCloud(format, state), ""
|
||||||
|
default:
|
||||||
|
return "", obrimDatetimeFailureInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeBuildOutput builds the standardized utility output.
|
||||||
|
func obrimDatetimeBuildOutput(
|
||||||
|
status bool,
|
||||||
|
code string,
|
||||||
|
payload any,
|
||||||
|
) map[string]any {
|
||||||
|
output := obrimDatetimeOutput{
|
||||||
|
Status: status,
|
||||||
|
Code: code,
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"status": output.Status,
|
||||||
|
"code": output.Code,
|
||||||
|
"payload": output.Payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeResolveState resolves trusted clock state through the clock service.
|
||||||
|
func obrimDatetimeResolveState() (obrimDatetimeClockState, bool) {
|
||||||
|
now := time.Now()
|
||||||
|
trusted := clock.ObrimClockCurrentClock()
|
||||||
|
|
||||||
|
offset := trusted.Sub(now.UTC())
|
||||||
|
|
||||||
|
if offset == 0 {
|
||||||
|
return obrimDatetimeClockState{
|
||||||
|
clockOffset: 0,
|
||||||
|
lastSync: 0,
|
||||||
|
}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimDatetimeClockState{
|
||||||
|
clockOffset: offset.Nanoseconds(),
|
||||||
|
lastSync: trusted.UnixNano(),
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeApplyOffset applies the resolved clock offset to system time.
|
||||||
|
func obrimDatetimeApplyOffset(value time.Time, state obrimDatetimeClockState) time.Time {
|
||||||
|
return value.Add(time.Duration(state.clockOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeFormat formats a datetime using a framework-supported pattern.
|
||||||
|
func obrimDatetimeFormat(value time.Time, format string) string {
|
||||||
|
switch format {
|
||||||
|
case obrimDatetimeFormatTimestampSec:
|
||||||
|
return formatTimestampSeconds(value)
|
||||||
|
|
||||||
|
case obrimDatetimeFormatTimestampMil:
|
||||||
|
return formatTimestampMilliseconds(value)
|
||||||
|
|
||||||
|
case obrimDatetimeFormatYearMonthDay:
|
||||||
|
return value.Format("2006-01-02")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatMonthDayYear:
|
||||||
|
return value.Format("Jan 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatFullMonthDayYear:
|
||||||
|
return value.Format("January 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatWeekdayMonthDayYear:
|
||||||
|
return value.Format("Mon, Jan 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatWeekdayFullMonthDayYear:
|
||||||
|
return value.Format("Mon, January 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatFullWeekdayMonthDayYear:
|
||||||
|
return value.Format("Monday, Jan 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatFullWeekdayFullMonthDayYear:
|
||||||
|
return value.Format("Monday, January 02, 2006")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatTime:
|
||||||
|
return value.Format("15:04:05")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatTime12:
|
||||||
|
return value.Format("03:04:05 PM")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatDateTime:
|
||||||
|
return value.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatDateTimeZone:
|
||||||
|
return value.Format("2006-01-02 15:04:05 MST")
|
||||||
|
|
||||||
|
case obrimDatetimeFormatISO8601:
|
||||||
|
return value.Format("2006-01-02T15:04:05Z07:00")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTimestampSeconds formats a datetime as a Unix timestamp in seconds.
|
||||||
|
func formatTimestampSeconds(value time.Time) string {
|
||||||
|
return value.Format("1136239445")
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTimestampMilliseconds formats a datetime as a Unix timestamp in milliseconds.
|
||||||
|
func formatTimestampMilliseconds(value time.Time) string {
|
||||||
|
return value.Format("1136239445123")
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeLocal retrieves and formats corrected local system time.
|
||||||
|
func obrimDatetimeLocal(
|
||||||
|
format string,
|
||||||
|
state obrimDatetimeClockState,
|
||||||
|
available bool,
|
||||||
|
) string {
|
||||||
|
value := time.Now()
|
||||||
|
|
||||||
|
if available {
|
||||||
|
value = obrimDatetimeApplyOffset(value, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimDatetimeFormat(value, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimDatetimeCloud retrieves and formats corrected trusted UTC time.
|
||||||
|
func obrimDatetimeCloud(
|
||||||
|
format string,
|
||||||
|
state obrimDatetimeClockState,
|
||||||
|
) string {
|
||||||
|
value := obrimDatetimeApplyOffset(time.Now().UTC(), state).UTC()
|
||||||
|
return obrimDatetimeFormat(value, format)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,396 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Hash
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Generate and verify cryptographic hash values using standard or
|
||||||
|
| salted hashing methods for integrity verification, canonical
|
||||||
|
| fingerprinting, resource identification, comparison workflows,
|
||||||
|
| and security-oriented hashing requirements.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use to generate deterministic hashes in standard mode.
|
||||||
|
| - Use to generate salted hashes when salted hashing is required.
|
||||||
|
| - Use to verify supplied hash values against input values.
|
||||||
|
| - Use standard mode when deterministic hashing without a salt is
|
||||||
|
| required.
|
||||||
|
| - Use salted mode when a supplied or cryptographically secure
|
||||||
|
| generated salt is required.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimHash("calculate", map[string]any{
|
||||||
|
| "mode": "standard",
|
||||||
|
| "algorithm": "...",
|
||||||
|
| "value": "...",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimHash("calculate", map[string]any{
|
||||||
|
| "mode": "salted",
|
||||||
|
| "algorithm": "...",
|
||||||
|
| "value": "...",
|
||||||
|
| "salt": "...",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimHash("compare", map[string]any{
|
||||||
|
| "mode": "standard",
|
||||||
|
| "algorithm": "...",
|
||||||
|
| "value": "...",
|
||||||
|
| "hash": "...",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimHash("compare", map[string]any{
|
||||||
|
| "mode": "salted",
|
||||||
|
| "algorithm": "...",
|
||||||
|
| "value": "...",
|
||||||
|
| "hash": "...",
|
||||||
|
| "salt": "...",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package hash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash result codes.
|
||||||
|
const (
|
||||||
|
ObrimHashSuccessCalculated = "SUCCESS_CALCULATED"
|
||||||
|
ObrimHashSuccessCompared = "SUCCESS_COMPARED"
|
||||||
|
ObrimHashFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
ObrimHashFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
ObrimHashFailureInvalidMode = "FAILURE_INVALID_MODE"
|
||||||
|
ObrimHashFailureInvalidAlgorithm = "FAILURE_INVALID_ALGORITHM"
|
||||||
|
ObrimHashFailureInvalidValue = "FAILURE_INVALID_VALUE"
|
||||||
|
ObrimHashFailureInvalidHash = "FAILURE_INVALID_HASH"
|
||||||
|
ObrimHashFailureInvalidSalt = "FAILURE_INVALID_SALT"
|
||||||
|
ObrimHashFailureProcessing = "FAILURE_PROCESSING"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash utility modes.
|
||||||
|
const (
|
||||||
|
obrimHashModeStandard = "standard"
|
||||||
|
obrimHashModeSalted = "salted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash utility algorithms.
|
||||||
|
const (
|
||||||
|
obrimHashAlgorithmSHA256 = "sha256"
|
||||||
|
obrimHashAlgorithmSHA512 = "sha512"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash utility configuration keys.
|
||||||
|
const (
|
||||||
|
obrimHashConfigMode = "mode"
|
||||||
|
obrimHashConfigAlgorithm = "algorithm"
|
||||||
|
obrimHashConfigValue = "value"
|
||||||
|
obrimHashConfigHash = "hash"
|
||||||
|
obrimHashConfigSalt = "salt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash utility operation types.
|
||||||
|
const (
|
||||||
|
obrimHashTypeCalculate = "calculate"
|
||||||
|
obrimHashTypeCompare = "compare"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash utility output structure.
|
||||||
|
type obrimHashOutput struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload map[string]any `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash utility request configuration.
|
||||||
|
type obrimHashConfig struct {
|
||||||
|
Mode string
|
||||||
|
Algorithm string
|
||||||
|
Value string
|
||||||
|
Hash string
|
||||||
|
Salt string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash utility execution function.
|
||||||
|
func ObrimHash(Type string, Config map[string]any) map[string]any {
|
||||||
|
ConfigData, Code := obrimHashValidateInput(Type, Config)
|
||||||
|
if Code != "" {
|
||||||
|
return obrimHashBuildOutput(false, Code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimHashRouteRequest(Type, ConfigData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hash utility input.
|
||||||
|
func obrimHashValidateInput(Type string, Config map[string]any) (obrimHashConfig, string) {
|
||||||
|
var ConfigData obrimHashConfig
|
||||||
|
|
||||||
|
switch Type {
|
||||||
|
case obrimHashTypeCalculate, obrimHashTypeCompare:
|
||||||
|
default:
|
||||||
|
return ConfigData, ObrimHashFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
if Config == nil {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigData.Mode = obrimHashReadStringConfig(Config, obrimHashConfigMode)
|
||||||
|
ConfigData.Algorithm = obrimHashReadStringConfig(Config, obrimHashConfigAlgorithm)
|
||||||
|
ConfigData.Value = obrimHashReadStringConfig(Config, obrimHashConfigValue)
|
||||||
|
ConfigData.Hash = obrimHashReadStringConfig(Config, obrimHashConfigHash)
|
||||||
|
ConfigData.Salt = obrimHashReadStringConfig(Config, obrimHashConfigSalt)
|
||||||
|
|
||||||
|
switch ConfigData.Mode {
|
||||||
|
case obrimHashModeStandard, obrimHashModeSalted:
|
||||||
|
default:
|
||||||
|
return ConfigData, ObrimHashFailureInvalidMode
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ConfigData.Algorithm {
|
||||||
|
case obrimHashAlgorithmSHA256, obrimHashAlgorithmSHA512:
|
||||||
|
default:
|
||||||
|
return ConfigData, ObrimHashFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
if ConfigData.Value == "" {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidValue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch Type {
|
||||||
|
case obrimHashTypeCalculate:
|
||||||
|
if ConfigData.Mode == obrimHashModeStandard && ConfigData.Salt != "" {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidSalt
|
||||||
|
}
|
||||||
|
case obrimHashTypeCompare:
|
||||||
|
if ConfigData.Hash == "" {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidHash
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ConfigData.Mode {
|
||||||
|
case obrimHashModeStandard:
|
||||||
|
if ConfigData.Salt != "" {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidSalt
|
||||||
|
}
|
||||||
|
case obrimHashModeSalted:
|
||||||
|
if ConfigData.Salt == "" {
|
||||||
|
return ConfigData, ObrimHashFailureInvalidSalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigData, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route the hash utility request.
|
||||||
|
func obrimHashRouteRequest(Type string, Config obrimHashConfig) map[string]any {
|
||||||
|
switch Type {
|
||||||
|
case obrimHashTypeCalculate:
|
||||||
|
return obrimHashCalculate(Config)
|
||||||
|
case obrimHashTypeCompare:
|
||||||
|
return obrimHashCompare(Config)
|
||||||
|
default:
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureInvalidType, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build standardized hash utility output.
|
||||||
|
func obrimHashBuildOutput(Status bool, Code string, Payload map[string]any) map[string]any {
|
||||||
|
Output := obrimHashOutput{
|
||||||
|
Status: Status,
|
||||||
|
Code: Code,
|
||||||
|
Payload: Payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"status": Output.Status,
|
||||||
|
"code": Output.Code,
|
||||||
|
"payload": Output.Payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process hash generation.
|
||||||
|
func obrimHashCalculate(Config obrimHashConfig) map[string]any {
|
||||||
|
switch Config.Mode {
|
||||||
|
case obrimHashModeStandard:
|
||||||
|
return obrimHashCalculateStandard(Config)
|
||||||
|
case obrimHashModeSalted:
|
||||||
|
return obrimHashCalculateSalted(Config)
|
||||||
|
default:
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureInvalidMode, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate deterministic standard hash.
|
||||||
|
func obrimHashCalculateStandard(Config obrimHashConfig) map[string]any {
|
||||||
|
HashValue, Error := obrimHashGenerate(Config.Algorithm, Config.Value)
|
||||||
|
if Error != nil {
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureProcessing, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Payload := map[string]any{
|
||||||
|
"algorithm": Config.Algorithm,
|
||||||
|
"hash": HashValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimHashBuildOutput(true, ObrimHashSuccessCalculated, Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate salted hash.
|
||||||
|
func obrimHashCalculateSalted(Config obrimHashConfig) map[string]any {
|
||||||
|
Salt := Config.Salt
|
||||||
|
|
||||||
|
if Salt == "" {
|
||||||
|
var Error error
|
||||||
|
|
||||||
|
Salt, Error = obrimHashGenerateSalt()
|
||||||
|
if Error != nil {
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureProcessing, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HashValue, Error := obrimHashGenerate(Config.Algorithm, Salt+Config.Value)
|
||||||
|
if Error != nil {
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureProcessing, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Payload := map[string]any{
|
||||||
|
"algorithm": Config.Algorithm,
|
||||||
|
"hash": HashValue,
|
||||||
|
"salt": Salt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimHashBuildOutput(true, ObrimHashSuccessCalculated, Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a cryptographically secure random salt.
|
||||||
|
func obrimHashGenerateSalt() (string, error) {
|
||||||
|
Salt := make([]byte, 32)
|
||||||
|
|
||||||
|
if _, Error := rand.Read(Salt); Error != nil {
|
||||||
|
return "", Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(Salt), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process hash verification.
|
||||||
|
func obrimHashCompare(Config obrimHashConfig) map[string]any {
|
||||||
|
switch Config.Mode {
|
||||||
|
case obrimHashModeStandard:
|
||||||
|
return obrimHashCompareStandard(Config)
|
||||||
|
case obrimHashModeSalted:
|
||||||
|
return obrimHashCompareSalted(Config)
|
||||||
|
default:
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureInvalidMode, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify standard hash.
|
||||||
|
func obrimHashCompareStandard(Config obrimHashConfig) map[string]any {
|
||||||
|
HashValue, Error := obrimHashGenerate(Config.Algorithm, Config.Value)
|
||||||
|
if Error != nil {
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureProcessing, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Matched := obrimHashVerify(HashValue, Config.Hash)
|
||||||
|
|
||||||
|
Payload := map[string]any{
|
||||||
|
"algorithm": Config.Algorithm,
|
||||||
|
"matched": Matched,
|
||||||
|
"hash": Config.Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimHashBuildOutput(true, ObrimHashSuccessCompared, Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify salted hash.
|
||||||
|
func obrimHashCompareSalted(Config obrimHashConfig) map[string]any {
|
||||||
|
HashValue, Error := obrimHashGenerate(Config.Algorithm, Config.Salt+Config.Value)
|
||||||
|
if Error != nil {
|
||||||
|
return obrimHashBuildOutput(false, ObrimHashFailureProcessing, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
Matched := obrimHashVerify(HashValue, Config.Hash)
|
||||||
|
|
||||||
|
Payload := map[string]any{
|
||||||
|
"algorithm": Config.Algorithm,
|
||||||
|
"matched": Matched,
|
||||||
|
"hash": Config.Hash,
|
||||||
|
"salt": Config.Salt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimHashBuildOutput(true, ObrimHashSuccessCompared, Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify regenerated and supplied hashes using strict byte comparison.
|
||||||
|
func obrimHashVerify(Expected string, Supplied string) bool {
|
||||||
|
return string([]byte(Expected)) == string([]byte(Supplied))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a hash using the selected algorithm.
|
||||||
|
func obrimHashGenerate(Algorithm string, Value string) (string, error) {
|
||||||
|
var HashFunction func() hash.Hash
|
||||||
|
|
||||||
|
switch Algorithm {
|
||||||
|
case obrimHashAlgorithmSHA256:
|
||||||
|
HashFunction = sha256.New
|
||||||
|
case obrimHashAlgorithmSHA512:
|
||||||
|
HashFunction = sha512.New
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported hashing algorithm")
|
||||||
|
}
|
||||||
|
|
||||||
|
Hasher := HashFunction()
|
||||||
|
|
||||||
|
if _, Error := Hasher.Write([]byte(Value)); Error != nil {
|
||||||
|
return "", Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(Hasher.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a string configuration value.
|
||||||
|
func obrimHashReadStringConfig(Config map[string]any, Key string) string {
|
||||||
|
Value, Exists := Config[Key]
|
||||||
|
if !Exists || Value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
StringValue, Valid := Value.(string)
|
||||||
|
if !Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringValue
|
||||||
|
}
|
||||||
@ -0,0 +1,445 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Key
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Generate cryptographic key material through a type-driven dispatch
|
||||||
|
| model supporting symmetric and asymmetric key generation.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use the utility to generate cryptographically secure symmetric or
|
||||||
|
| asymmetric key material.
|
||||||
|
| - Use the "symmetric" type with the supported AES algorithm and
|
||||||
|
| 128, 192, or 256 bit key sizes.
|
||||||
|
| - Use the "asymmetric" type with the supported ECC algorithm using
|
||||||
|
| the EdDSA curve.
|
||||||
|
| - Provide the type-specific configuration required for the selected
|
||||||
|
| key generation workflow.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimKey("symmetric", map[string]any{
|
||||||
|
| "algorithm": "aes",
|
||||||
|
| "key_size": 256,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimKey("asymmetric", map[string]any{
|
||||||
|
| "algorithm": "ecc",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimKeyTypeSymmetric = "symmetric"
|
||||||
|
obrimKeyTypeAsymmetric = "asymmetric"
|
||||||
|
|
||||||
|
obrimKeyAlgorithmAES = "aes"
|
||||||
|
obrimKeyAlgorithmECC = "ecc"
|
||||||
|
obrimKeyCurveEdDSA = "eddsa"
|
||||||
|
|
||||||
|
obrimKeySize128 = 128
|
||||||
|
obrimKeySize192 = 192
|
||||||
|
obrimKeySize256 = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimKeySuccessSymmetricGenerated = "SUCCESS_SYMMETRIC_KEY_GENERATED"
|
||||||
|
obrimKeySuccessAsymmetricGenerated = "SUCCESS_ASYMMETRIC_KEY_GENERATED"
|
||||||
|
|
||||||
|
obrimKeyFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimKeyFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
obrimKeyFailureMissingAlgorithm = "FAILURE_MISSING_ALGORITHM"
|
||||||
|
obrimKeyFailureInvalidAlgorithm = "FAILURE_INVALID_ALGORITHM"
|
||||||
|
obrimKeyFailureMissingKeySize = "FAILURE_MISSING_KEY_SIZE"
|
||||||
|
obrimKeyFailureInvalidKeySize = "FAILURE_INVALID_KEY_SIZE"
|
||||||
|
obrimKeyFailureKeyGeneration = "FAILURE_KEY_GENERATION"
|
||||||
|
obrimKeyFailureAsymmetricKeyGeneration = "FAILURE_ASYMMETRIC_KEY_GENERATION"
|
||||||
|
obrimKeyFailureUnsupportedOperation = "FAILURE_UNSUPPORTED_OPERATION"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimKeyConfigAlgorithm = "algorithm"
|
||||||
|
obrimKeyConfigKeySize = "key_size"
|
||||||
|
)
|
||||||
|
|
||||||
|
type obrimKeySymmetricConfig struct {
|
||||||
|
algorithm string
|
||||||
|
keySize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimKeyAsymmetricConfig struct {
|
||||||
|
algorithm string
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimKeySymmetricPayload struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
KeySize int `json:"key_size"`
|
||||||
|
KeyMaterial string `json:"key_material"`
|
||||||
|
GeneratedAt string `json:"generated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimKeyAsymmetricPayload struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
Curve string `json:"curve"`
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
GeneratedAt string `json:"generated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimKeyOutput struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload any `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ObrimKey(keyType string, config map[string]any) map[string]any {
|
||||||
|
if code := obrimKeyValidateInput(keyType, config); code != "" {
|
||||||
|
return obrimKeyBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimKeyRouteRequest(keyType, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyValidateInput(keyType string, config map[string]any) string {
|
||||||
|
switch keyType {
|
||||||
|
case obrimKeyTypeSymmetric:
|
||||||
|
if config == nil {
|
||||||
|
return obrimKeyFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm, ok := config[obrimKeyConfigAlgorithm]
|
||||||
|
if !ok {
|
||||||
|
return obrimKeyFailureMissingAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithmValue, ok := algorithm.(string)
|
||||||
|
if !ok || algorithmValue == "" {
|
||||||
|
return obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
switch algorithmValue {
|
||||||
|
case obrimKeyAlgorithmAES:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
keySize, ok := config[obrimKeyConfigKeySize]
|
||||||
|
if !ok {
|
||||||
|
return obrimKeyFailureMissingKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := keySize.(type) {
|
||||||
|
case int:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case int8:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case int16:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case int32:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case uint:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case uint8:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case uint16:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case uint32:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
case uint64:
|
||||||
|
switch value {
|
||||||
|
case obrimKeySize128, obrimKeySize192, obrimKeySize256:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
case obrimKeyTypeAsymmetric:
|
||||||
|
if config == nil {
|
||||||
|
return obrimKeyFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm, ok := config[obrimKeyConfigAlgorithm]
|
||||||
|
if !ok {
|
||||||
|
return obrimKeyFailureMissingAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithmValue, ok := algorithm.(string)
|
||||||
|
if !ok || algorithmValue == "" {
|
||||||
|
return obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
switch algorithmValue {
|
||||||
|
case obrimKeyAlgorithmECC:
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return obrimKeyFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyRouteRequest(keyType string, config map[string]any) map[string]any {
|
||||||
|
switch keyType {
|
||||||
|
case obrimKeyTypeSymmetric:
|
||||||
|
normalized, code := obrimKeyNormalizeSymmetricConfig(config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimKeyBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, code := obrimKeyGenerateSymmetric(normalized)
|
||||||
|
if code != "" {
|
||||||
|
return obrimKeyBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimKeyBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimKeySuccessSymmetricGenerated,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
case obrimKeyTypeAsymmetric:
|
||||||
|
normalized, code := obrimKeyNormalizeAsymmetricConfig(config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimKeyBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, code := obrimKeyGenerateAsymmetric(normalized)
|
||||||
|
if code != "" {
|
||||||
|
return obrimKeyBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimKeyBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimKeySuccessAsymmetricGenerated,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return obrimKeyBuildOutput(false, obrimKeyFailureUnsupportedOperation, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyBuildOutput(status bool, code string, payload any) map[string]any {
|
||||||
|
output := obrimKeyOutput{
|
||||||
|
Status: status,
|
||||||
|
Code: code,
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"status": output.Status,
|
||||||
|
"code": output.Code,
|
||||||
|
"payload": output.Payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyNormalizeSymmetricConfig(config map[string]any) (obrimKeySymmetricConfig, string) {
|
||||||
|
algorithm, ok := config[obrimKeyConfigAlgorithm].(string)
|
||||||
|
if !ok || algorithm == "" {
|
||||||
|
return obrimKeySymmetricConfig{}, obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
keySizeValue, ok := config[obrimKeyConfigKeySize]
|
||||||
|
if !ok {
|
||||||
|
return obrimKeySymmetricConfig{}, obrimKeyFailureMissingKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
keySize, ok := obrimKeyNormalizeKeySize(keySizeValue)
|
||||||
|
if !ok {
|
||||||
|
return obrimKeySymmetricConfig{}, obrimKeyFailureInvalidKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimKeySymmetricConfig{
|
||||||
|
algorithm: algorithm,
|
||||||
|
keySize: keySize,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyNormalizeKeySize(value any) (int, bool) {
|
||||||
|
switch keySize := value.(type) {
|
||||||
|
case int:
|
||||||
|
return keySize, true
|
||||||
|
case int8:
|
||||||
|
return int(keySize), true
|
||||||
|
case int16:
|
||||||
|
return int(keySize), true
|
||||||
|
case int32:
|
||||||
|
return int(keySize), true
|
||||||
|
case int64:
|
||||||
|
return int(keySize), true
|
||||||
|
case uint:
|
||||||
|
return int(keySize), true
|
||||||
|
case uint8:
|
||||||
|
return int(keySize), true
|
||||||
|
case uint16:
|
||||||
|
return int(keySize), true
|
||||||
|
case uint32:
|
||||||
|
return int(keySize), true
|
||||||
|
case uint64:
|
||||||
|
return int(keySize), true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyGenerateSymmetric(config obrimKeySymmetricConfig) (map[string]any, string) {
|
||||||
|
switch config.algorithm {
|
||||||
|
case obrimKeyAlgorithmAES:
|
||||||
|
return obrimKeyGenerateAES(config.keySize)
|
||||||
|
default:
|
||||||
|
return nil, obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyGenerateAES(keySize int) (map[string]any, string) {
|
||||||
|
keyLength := keySize / 8
|
||||||
|
|
||||||
|
keyMaterial := make([]byte, keyLength)
|
||||||
|
if _, err := rand.Read(keyMaterial); err != nil {
|
||||||
|
return nil, obrimKeyFailureKeyGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := aes.NewCipher(keyMaterial); err != nil {
|
||||||
|
return nil, obrimKeyFailureKeyGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := obrimKeySymmetricPayload{
|
||||||
|
Type: obrimKeyTypeSymmetric,
|
||||||
|
Algorithm: obrimKeyAlgorithmAES,
|
||||||
|
KeySize: keySize,
|
||||||
|
KeyMaterial: base64.StdEncoding.EncodeToString(keyMaterial),
|
||||||
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"type": payload.Type,
|
||||||
|
"algorithm": payload.Algorithm,
|
||||||
|
"key_size": payload.KeySize,
|
||||||
|
"key_material": payload.KeyMaterial,
|
||||||
|
"generated_at": payload.GeneratedAt,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyNormalizeAsymmetricConfig(config map[string]any) (obrimKeyAsymmetricConfig, string) {
|
||||||
|
algorithm, ok := config[obrimKeyConfigAlgorithm].(string)
|
||||||
|
if !ok || algorithm == "" {
|
||||||
|
return obrimKeyAsymmetricConfig{}, obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimKeyAsymmetricConfig{
|
||||||
|
algorithm: algorithm,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyGenerateAsymmetric(config obrimKeyAsymmetricConfig) (map[string]any, string) {
|
||||||
|
switch config.algorithm {
|
||||||
|
case obrimKeyAlgorithmECC:
|
||||||
|
return obrimKeyGenerateECC()
|
||||||
|
default:
|
||||||
|
return nil, obrimKeyFailureInvalidAlgorithm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func obrimKeyGenerateECC() (map[string]any, string) {
|
||||||
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, obrimKeyFailureAsymmetricKeyGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := obrimKeyAsymmetricPayload{
|
||||||
|
Type: obrimKeyTypeAsymmetric,
|
||||||
|
Algorithm: obrimKeyAlgorithmECC,
|
||||||
|
Curve: obrimKeyCurveEdDSA,
|
||||||
|
PublicKey: base64.StdEncoding.EncodeToString(publicKey),
|
||||||
|
PrivateKey: base64.StdEncoding.EncodeToString(privateKey),
|
||||||
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"type": payload.Type,
|
||||||
|
"algorithm": payload.Algorithm,
|
||||||
|
"curve": payload.Curve,
|
||||||
|
"public_key": payload.PublicKey,
|
||||||
|
"private_key": payload.PrivateKey,
|
||||||
|
"generated_at": payload.GeneratedAt,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Log
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provide a framework-level logging utility that records structured
|
||||||
|
| plaintext log entries to a persistent filesystem location using a
|
||||||
|
| deterministic and platform-aware storage strategy.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use to write standardized log messages from framework services and
|
||||||
|
| software features.
|
||||||
|
| - Provide a logical source identifier using the SERVICENAME/UTILITYNAME
|
||||||
|
| or SERVICENAME/FEATURENAME format.
|
||||||
|
| - Provide the human-readable log message as plain, formatted, or any
|
||||||
|
| UTF-8 string.
|
||||||
|
| - Log entries are stored in the platform-specific persistent log
|
||||||
|
| directory using the software name as the directory name.
|
||||||
|
| - Log entries are appended as complete UTF-8 plaintext lines without
|
||||||
|
| overwriting existing content.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimLog(
|
||||||
|
| "SERVICENAME/UTILITYNAME",
|
||||||
|
| "Utility operation completed",
|
||||||
|
| )
|
||||||
|
| - ObrimLog(
|
||||||
|
| "SERVICENAME/FEATURENAME",
|
||||||
|
| "Feature execution started",
|
||||||
|
| )
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// obrimLogSoftwareName identifies the software owning the log directory.
|
||||||
|
const obrimLogSoftwareName = "software"
|
||||||
|
|
||||||
|
// ObrimLog writes a standardized log message.
|
||||||
|
func ObrimLog(label, message string) {
|
||||||
|
obrimLogLabel, obrimLogMessage, obrimLogValid := obrimLogValidateInput(label, message)
|
||||||
|
if !obrimLogValid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogName := obrimLogResolveName()
|
||||||
|
if obrimLogName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogDirectory, obrimLogDirectoryValid := obrimLogResolveDirectory(obrimLogName)
|
||||||
|
if !obrimLogDirectoryValid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogFile, obrimLogFileValid := obrimLogResolveFile(obrimLogDirectory)
|
||||||
|
if !obrimLogFileValid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogTimestamp := obrimLogGetTimestamp()
|
||||||
|
obrimLogEntry := obrimLogBuildEntry(obrimLogTimestamp, obrimLogLabel, obrimLogMessage)
|
||||||
|
|
||||||
|
obrimLogWriteEntry(obrimLogFile, obrimLogEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogValidateInput normalizes and validates the log input.
|
||||||
|
func obrimLogValidateInput(label, message string) (string, string, bool) {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
message = strings.ReplaceAll(message, "\r\n", "\n")
|
||||||
|
message = strings.ReplaceAll(message, "\r", "\n")
|
||||||
|
message = strings.ReplaceAll(message, "\n", " ")
|
||||||
|
|
||||||
|
if label == "" || message == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return label, message, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogResolveName retrieves the software name from the local constant.
|
||||||
|
func obrimLogResolveName() string {
|
||||||
|
return strings.TrimSpace(obrimLogSoftwareName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogResolveDirectory resolves and creates the platform-specific log directory.
|
||||||
|
func obrimLogResolveDirectory(softwareName string) (string, bool) {
|
||||||
|
var obrimLogDirectory string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
obrimLogHome, obrimLogHomeError := os.UserHomeDir()
|
||||||
|
if obrimLogHomeError != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogDirectory = filepath.Join(
|
||||||
|
obrimLogHome,
|
||||||
|
".local",
|
||||||
|
"state",
|
||||||
|
softwareName,
|
||||||
|
"log",
|
||||||
|
"main",
|
||||||
|
)
|
||||||
|
|
||||||
|
case "windows":
|
||||||
|
obrimLogLocalAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA"))
|
||||||
|
if obrimLogLocalAppData == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogDirectory = filepath.Join(
|
||||||
|
obrimLogLocalAppData,
|
||||||
|
softwareName,
|
||||||
|
"log",
|
||||||
|
"main",
|
||||||
|
)
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
obrimLogHome, obrimLogHomeError := os.UserHomeDir()
|
||||||
|
if obrimLogHomeError != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimLogDirectory = filepath.Join(
|
||||||
|
obrimLogHome,
|
||||||
|
"Library",
|
||||||
|
"Logs",
|
||||||
|
softwareName,
|
||||||
|
"log",
|
||||||
|
"main",
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimLogDirectory == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimLogMkdirError := os.MkdirAll(obrimLogDirectory, 0o755); obrimLogMkdirError != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimLogDirectory, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogResolveFile resolves and creates the main log file.
|
||||||
|
func obrimLogResolveFile(directory string) (string, bool) {
|
||||||
|
obrimLogFile := filepath.Join(directory, "main.log")
|
||||||
|
|
||||||
|
obrimLogHandle, obrimLogOpenError := os.OpenFile(
|
||||||
|
obrimLogFile,
|
||||||
|
os.O_CREATE|os.O_APPEND|os.O_WRONLY,
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if obrimLogOpenError != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obrimLogCloseError := obrimLogHandle.Close(); obrimLogCloseError != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimLogFile, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogGetTimestamp gets the timestamp for a log entry.
|
||||||
|
func obrimLogGetTimestamp() string {
|
||||||
|
return time.Now().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogBuildEntry builds a complete formatted log entry.
|
||||||
|
func obrimLogBuildEntry(timestamp, label, message string) string {
|
||||||
|
return "[" + timestamp + "] [" + label + "] " + message + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimLogWriteEntry appends a complete log entry to the log file.
|
||||||
|
func obrimLogWriteEntry(file, entry string) {
|
||||||
|
obrimLogHandle, obrimLogOpenError := os.OpenFile(
|
||||||
|
file,
|
||||||
|
os.O_APPEND|os.O_WRONLY,
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if obrimLogOpenError != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer obrimLogHandle.Close()
|
||||||
|
|
||||||
|
_, _ = obrimLogHandle.WriteString(entry)
|
||||||
|
}
|
||||||
@ -0,0 +1,627 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Marker
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provide a unified utility for generating unique markers through
|
||||||
|
| multiple generation strategies using a single entry point.
|
||||||
|
| - Support time-based, random, and encoded marker generation while
|
||||||
|
| maintaining a consistent output structure, deterministic routing,
|
||||||
|
| and extensible architecture.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use the time type with an epoch timestamp and instance identifier
|
||||||
|
| to generate distributed time-based markers.
|
||||||
|
| - Use the random type with a desired length and character set to
|
||||||
|
| generate collision-resistant random markers.
|
||||||
|
| - Use the encoded type with source data, output length, character
|
||||||
|
| set, and optional salt to generate deterministic encoded markers.
|
||||||
|
| - Validate the marker type and all type-specific configuration
|
||||||
|
| values before processing.
|
||||||
|
| - Use the unified ObrimMarker entry point to execute marker
|
||||||
|
| generation.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimMarker("time", map[string]any{
|
||||||
|
| "epoch": int64(0),
|
||||||
|
| "instance": "instance-01",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimMarker("random", map[string]any{
|
||||||
|
| "length": 32,
|
||||||
|
| "charset": "abcdefghijklmnopqrstuvwxyz0123456789",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimMarker("encoded", map[string]any{
|
||||||
|
| "data": "source-data",
|
||||||
|
| "length": 32,
|
||||||
|
| "charset": "abcdefghijklmnopqrstuvwxyz0123456789",
|
||||||
|
| "salt": "optional-salt",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package marker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Marker type identifiers.
|
||||||
|
obrimMarkerTypeTime = "time"
|
||||||
|
obrimMarkerTypeRandom = "random"
|
||||||
|
obrimMarkerTypeEncoded = "encoded"
|
||||||
|
|
||||||
|
// Default marker configuration values.
|
||||||
|
obrimMarkerDefaultEpoch int64 = 0
|
||||||
|
obrimMarkerDefaultCharset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
obrimMarkerDefaultLength int = 32
|
||||||
|
|
||||||
|
// Marker result codes.
|
||||||
|
obrimMarkerSuccessTime = "SUCCESS_TIME_MARKER_GENERATED"
|
||||||
|
obrimMarkerSuccessRandom = "SUCCESS_RANDOM_MARKER_GENERATED"
|
||||||
|
obrimMarkerSuccessEncoded = "SUCCESS_ENCODED_MARKER_GENERATED"
|
||||||
|
|
||||||
|
// Marker failure codes.
|
||||||
|
obrimMarkerFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimMarkerFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
obrimMarkerFailureInvalidEpoch = "FAILURE_INVALID_EPOCH"
|
||||||
|
obrimMarkerFailureInvalidInstance = "FAILURE_INVALID_INSTANCE"
|
||||||
|
obrimMarkerFailureInvalidLength = "FAILURE_INVALID_LENGTH"
|
||||||
|
obrimMarkerFailureInvalidCharset = "FAILURE_INVALID_CHARSET"
|
||||||
|
obrimMarkerFailureEmptySource = "FAILURE_EMPTY_SOURCE"
|
||||||
|
obrimMarkerFailureInvalidEncodingConfig = "FAILURE_INVALID_ENCODING_CONFIG"
|
||||||
|
obrimMarkerFailureGenerationError = "FAILURE_GENERATION_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
type obrimMarkerOutput struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload any `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerTimePayload struct {
|
||||||
|
Marker string `json:"marker"`
|
||||||
|
Epoch int64 `json:"epoch"`
|
||||||
|
Instance string `json:"instance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerRandomPayload struct {
|
||||||
|
Marker string `json:"marker"`
|
||||||
|
Length int `json:"length"`
|
||||||
|
Charset string `json:"charset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerEncodedPayload struct {
|
||||||
|
Marker string `json:"marker"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Length int `json:"length"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerTimeConfig struct {
|
||||||
|
Epoch int64
|
||||||
|
Instance string
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerRandomConfig struct {
|
||||||
|
Length int
|
||||||
|
Charset string
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimMarkerEncodedConfig struct {
|
||||||
|
Data string
|
||||||
|
Length int
|
||||||
|
Charset string
|
||||||
|
Salt string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimMarker generates a marker using the requested generation strategy.
|
||||||
|
func ObrimMarker(typeName string, config map[string]any) map[string]any {
|
||||||
|
if code := obrimMarkerValidateInput(typeName, config); code != "" {
|
||||||
|
return obrimMarkerBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, code := obrimMarkerRouteRequest(typeName, config)
|
||||||
|
if code != "" {
|
||||||
|
return obrimMarkerBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerBuildOutput(true, code, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerValidateInput validates the requested marker type and configuration.
|
||||||
|
func obrimMarkerValidateInput(typeName string, config map[string]any) string {
|
||||||
|
if config == nil {
|
||||||
|
return obrimMarkerFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typeName {
|
||||||
|
case obrimMarkerTypeTime:
|
||||||
|
if code := obrimMarkerValidateTime(config); code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
case obrimMarkerTypeRandom:
|
||||||
|
if code := obrimMarkerValidateRandom(config); code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
case obrimMarkerTypeEncoded:
|
||||||
|
if code := obrimMarkerValidateEncoded(config); code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return obrimMarkerFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerRouteRequest routes the request to its type-specific workflow.
|
||||||
|
func obrimMarkerRouteRequest(typeName string, config map[string]any) (any, string) {
|
||||||
|
switch typeName {
|
||||||
|
case obrimMarkerTypeTime:
|
||||||
|
return obrimMarkerTime(config)
|
||||||
|
case obrimMarkerTypeRandom:
|
||||||
|
return obrimMarkerRandom(config)
|
||||||
|
case obrimMarkerTypeEncoded:
|
||||||
|
return obrimMarkerEncoded(config)
|
||||||
|
default:
|
||||||
|
return nil, obrimMarkerFailureInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerBuildOutput builds the standardized utility output.
|
||||||
|
func obrimMarkerBuildOutput(status bool, code string, payload any) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"code": code,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerTime executes the time marker workflow.
|
||||||
|
func obrimMarkerTime(config map[string]any) (any, string) {
|
||||||
|
markerConfig, code := obrimMarkerTimeConfig(config)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
marker, code := obrimMarkerTimeGenerate(markerConfig)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerTimePayload{
|
||||||
|
Marker: marker,
|
||||||
|
Epoch: markerConfig.Epoch,
|
||||||
|
Instance: markerConfig.Instance,
|
||||||
|
}, obrimMarkerSuccessTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerValidateTime validates time marker configuration.
|
||||||
|
func obrimMarkerValidateTime(config map[string]any) string {
|
||||||
|
epoch := obrimMarkerDefaultEpoch
|
||||||
|
if value, exists := config["epoch"]; exists {
|
||||||
|
parsed, ok := obrimMarkerInt64(value)
|
||||||
|
if !ok || parsed < 0 {
|
||||||
|
return obrimMarkerFailureInvalidEpoch
|
||||||
|
}
|
||||||
|
epoch = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if epoch > time.Now().UnixNano() {
|
||||||
|
return obrimMarkerFailureInvalidEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
instance, exists := config["instance"]
|
||||||
|
if !exists {
|
||||||
|
return obrimMarkerFailureInvalidInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceValue, ok := instance.(string)
|
||||||
|
if !ok || strings.TrimSpace(instanceValue) == "" {
|
||||||
|
return obrimMarkerFailureInvalidInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerTimeConfig builds the normalized time marker configuration.
|
||||||
|
func obrimMarkerTimeConfig(config map[string]any) (obrimMarkerTimeConfig, string) {
|
||||||
|
epoch := obrimMarkerDefaultEpoch
|
||||||
|
if value, exists := config["epoch"]; exists {
|
||||||
|
parsed, ok := obrimMarkerInt64(value)
|
||||||
|
if !ok || parsed < 0 || parsed > time.Now().UnixNano() {
|
||||||
|
return obrimMarkerTimeConfig{}, obrimMarkerFailureInvalidEpoch
|
||||||
|
}
|
||||||
|
epoch = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
instance, ok := config["instance"].(string)
|
||||||
|
if !ok || strings.TrimSpace(instance) == "" {
|
||||||
|
return obrimMarkerTimeConfig{}, obrimMarkerFailureInvalidInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerTimeConfig{
|
||||||
|
Epoch: epoch,
|
||||||
|
Instance: strings.TrimSpace(instance),
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerTimeGenerate generates a distributed time-based marker.
|
||||||
|
func obrimMarkerTimeGenerate(config obrimMarkerTimeConfig) (string, string) {
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
elapsed := now - config.Epoch
|
||||||
|
|
||||||
|
if elapsed < 0 {
|
||||||
|
return "", obrimMarkerFailureInvalidEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := fmt.Sprintf(
|
||||||
|
"%x-%s",
|
||||||
|
elapsed,
|
||||||
|
config.Instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return marker, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerRandom executes the random marker workflow.
|
||||||
|
func obrimMarkerRandom(config map[string]any) (any, string) {
|
||||||
|
markerConfig, code := obrimMarkerRandomConfig(config)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
marker, code := obrimMarkerRandomGenerate(markerConfig)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerRandomPayload{
|
||||||
|
Marker: marker,
|
||||||
|
Length: markerConfig.Length,
|
||||||
|
Charset: markerConfig.Charset,
|
||||||
|
}, obrimMarkerSuccessRandom
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerValidateRandom validates random marker configuration.
|
||||||
|
func obrimMarkerValidateRandom(config map[string]any) string {
|
||||||
|
length := obrimMarkerDefaultLength
|
||||||
|
if value, exists := config["length"]; exists {
|
||||||
|
parsed, ok := obrimMarkerInt(value)
|
||||||
|
if !ok || parsed <= 0 {
|
||||||
|
return obrimMarkerFailureInvalidLength
|
||||||
|
}
|
||||||
|
length = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if length <= 0 {
|
||||||
|
return obrimMarkerFailureInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
charset := obrimMarkerDefaultCharset
|
||||||
|
if value, exists := config["charset"]; exists {
|
||||||
|
parsed, ok := value.(string)
|
||||||
|
if !ok || parsed == "" {
|
||||||
|
return obrimMarkerFailureInvalidCharset
|
||||||
|
}
|
||||||
|
charset = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if charset == "" {
|
||||||
|
return obrimMarkerFailureInvalidCharset
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerRandomConfig builds the normalized random marker configuration.
|
||||||
|
func obrimMarkerRandomConfig(config map[string]any) (obrimMarkerRandomConfig, string) {
|
||||||
|
length := obrimMarkerDefaultLength
|
||||||
|
if value, exists := config["length"]; exists {
|
||||||
|
parsed, ok := obrimMarkerInt(value)
|
||||||
|
if !ok || parsed <= 0 {
|
||||||
|
return obrimMarkerRandomConfig{}, obrimMarkerFailureInvalidLength
|
||||||
|
}
|
||||||
|
length = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
charset := obrimMarkerDefaultCharset
|
||||||
|
if value, exists := config["charset"]; exists {
|
||||||
|
parsed, ok := value.(string)
|
||||||
|
if !ok || parsed == "" {
|
||||||
|
return obrimMarkerRandomConfig{}, obrimMarkerFailureInvalidCharset
|
||||||
|
}
|
||||||
|
charset = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if charset == "" {
|
||||||
|
return obrimMarkerRandomConfig{}, obrimMarkerFailureInvalidCharset
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerRandomConfig{
|
||||||
|
Length: length,
|
||||||
|
Charset: charset,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerRandomGenerate generates a cryptographically secure random marker.
|
||||||
|
func obrimMarkerRandomGenerate(config obrimMarkerRandomConfig) (string, string) {
|
||||||
|
characters := []rune(config.Charset)
|
||||||
|
if len(characters) == 0 {
|
||||||
|
return "", obrimMarkerFailureInvalidCharset
|
||||||
|
}
|
||||||
|
|
||||||
|
randomBytes := make([]byte, config.Length)
|
||||||
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
|
return "", obrimMarkerFailureGenerationError
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := make([]rune, config.Length)
|
||||||
|
for index := range marker {
|
||||||
|
marker[index] = characters[int(randomBytes[index])%len(characters)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(marker), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerEncoded executes the encoded marker workflow.
|
||||||
|
func obrimMarkerEncoded(config map[string]any) (any, string) {
|
||||||
|
markerConfig, code := obrimMarkerEncodedConfig(config)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
marker, code := obrimMarkerEncodedGenerate(markerConfig)
|
||||||
|
if code != "" {
|
||||||
|
return nil, code
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerEncodedPayload{
|
||||||
|
Marker: marker,
|
||||||
|
Source: markerConfig.Data,
|
||||||
|
Length: markerConfig.Length,
|
||||||
|
}, obrimMarkerSuccessEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerValidateEncoded validates encoded marker configuration.
|
||||||
|
func obrimMarkerValidateEncoded(config map[string]any) string {
|
||||||
|
data, exists := config["data"]
|
||||||
|
if !exists {
|
||||||
|
return obrimMarkerFailureEmptySource
|
||||||
|
}
|
||||||
|
|
||||||
|
dataValue, ok := data.(string)
|
||||||
|
if !ok || strings.TrimSpace(dataValue) == "" {
|
||||||
|
return obrimMarkerFailureEmptySource
|
||||||
|
}
|
||||||
|
|
||||||
|
length := obrimMarkerDefaultLength
|
||||||
|
if value, exists := config["length"]; exists {
|
||||||
|
parsed, ok := obrimMarkerInt(value)
|
||||||
|
if !ok || parsed <= 0 {
|
||||||
|
return obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
length = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if length <= 0 {
|
||||||
|
return obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
charset := obrimMarkerDefaultCharset
|
||||||
|
if value, exists := config["charset"]; exists {
|
||||||
|
parsed, ok := value.(string)
|
||||||
|
if !ok || parsed == "" {
|
||||||
|
return obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
charset = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if charset == "" {
|
||||||
|
return obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, exists := config["salt"]; exists {
|
||||||
|
if _, ok := value.(string); !ok {
|
||||||
|
return obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = length
|
||||||
|
_ = charset
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerEncodedConfig builds the normalized encoded marker configuration.
|
||||||
|
func obrimMarkerEncodedConfig(config map[string]any) (obrimMarkerEncodedConfig, string) {
|
||||||
|
data, ok := config["data"].(string)
|
||||||
|
if !ok || strings.TrimSpace(data) == "" {
|
||||||
|
return obrimMarkerEncodedConfig{}, obrimMarkerFailureEmptySource
|
||||||
|
}
|
||||||
|
|
||||||
|
length := obrimMarkerDefaultLength
|
||||||
|
if value, exists := config["length"]; exists {
|
||||||
|
parsed, valid := obrimMarkerInt(value)
|
||||||
|
if !valid || parsed <= 0 {
|
||||||
|
return obrimMarkerEncodedConfig{}, obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
length = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
charset := obrimMarkerDefaultCharset
|
||||||
|
if value, exists := config["charset"]; exists {
|
||||||
|
parsed, valid := value.(string)
|
||||||
|
if !valid || parsed == "" {
|
||||||
|
return obrimMarkerEncodedConfig{}, obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
charset = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
salt := ""
|
||||||
|
if value, exists := config["salt"]; exists {
|
||||||
|
parsed, valid := value.(string)
|
||||||
|
if !valid {
|
||||||
|
return obrimMarkerEncodedConfig{}, obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
salt = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimMarkerEncodedConfig{
|
||||||
|
Data: data,
|
||||||
|
Length: length,
|
||||||
|
Charset: charset,
|
||||||
|
Salt: salt,
|
||||||
|
}, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerEncodedGenerate generates a deterministic encoded marker.
|
||||||
|
func obrimMarkerEncodedGenerate(config obrimMarkerEncodedConfig) (string, string) {
|
||||||
|
characters := []rune(config.Charset)
|
||||||
|
if len(characters) == 0 {
|
||||||
|
return "", obrimMarkerFailureInvalidEncodingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
source := config.Data + config.Salt
|
||||||
|
digest := sha256.Sum256([]byte(source))
|
||||||
|
|
||||||
|
seed := binary.BigEndian.Uint64(digest[:8])
|
||||||
|
marker := make([]rune, config.Length)
|
||||||
|
|
||||||
|
for index := range marker {
|
||||||
|
seed = seed*6364136223846793005 + 1442695040888963407
|
||||||
|
marker[index] = characters[int(seed%uint64(len(characters)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(marker), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerInt converts a supported value to int.
|
||||||
|
func obrimMarkerInt(value any) (int, bool) {
|
||||||
|
switch parsed := value.(type) {
|
||||||
|
case int:
|
||||||
|
return parsed, true
|
||||||
|
case int8:
|
||||||
|
return int(parsed), true
|
||||||
|
case int16:
|
||||||
|
return int(parsed), true
|
||||||
|
case int32:
|
||||||
|
return int(parsed), true
|
||||||
|
case int64:
|
||||||
|
return int(parsed), true
|
||||||
|
case uint:
|
||||||
|
return int(parsed), true
|
||||||
|
case uint8:
|
||||||
|
return int(parsed), true
|
||||||
|
case uint16:
|
||||||
|
return int(parsed), true
|
||||||
|
case uint32:
|
||||||
|
return int(parsed), true
|
||||||
|
case uint64:
|
||||||
|
if uint64(int(parsed)) != parsed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(parsed), true
|
||||||
|
case float32:
|
||||||
|
if float32(int(parsed)) != parsed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(parsed), true
|
||||||
|
case float64:
|
||||||
|
if float64(int(parsed)) != parsed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(parsed), true
|
||||||
|
case string:
|
||||||
|
converted, err := strconv.Atoi(parsed)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return converted, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimMarkerInt64 converts a supported value to int64.
|
||||||
|
func obrimMarkerInt64(value any) (int64, bool) {
|
||||||
|
switch parsed := value.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(parsed), true
|
||||||
|
case int8:
|
||||||
|
return int64(parsed), true
|
||||||
|
case int16:
|
||||||
|
return int64(parsed), true
|
||||||
|
case int32:
|
||||||
|
return int64(parsed), true
|
||||||
|
case int64:
|
||||||
|
return parsed, true
|
||||||
|
case uint:
|
||||||
|
if uint64(int64(parsed)) != uint64(parsed) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int64(parsed), true
|
||||||
|
case uint8:
|
||||||
|
return int64(parsed), true
|
||||||
|
case uint16:
|
||||||
|
return int64(parsed), true
|
||||||
|
case uint32:
|
||||||
|
return int64(parsed), true
|
||||||
|
case uint64:
|
||||||
|
if parsed > uint64(^uint64(0)>>1) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int64(parsed), true
|
||||||
|
case float32:
|
||||||
|
if float32(int64(parsed)) != parsed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int64(parsed), true
|
||||||
|
case float64:
|
||||||
|
if float64(int64(parsed)) != parsed {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int64(parsed), true
|
||||||
|
case string:
|
||||||
|
converted, err := strconv.ParseInt(parsed, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return converted, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Progress
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Manage, monitor, and report lifecycle-aware task progress through
|
||||||
|
| standardized progress tracking for countable and uncountable workflows.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use countable progress when tracking numeric progress using current
|
||||||
|
| and target values.
|
||||||
|
| - Use uncountable progress when tracking activity-based progress
|
||||||
|
| without percentage calculation.
|
||||||
|
| - Provide the lifecycle state through the state configuration value.
|
||||||
|
| - Provide current and target values when using countable progress.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimProgress("countable", map[string]any{
|
||||||
|
| "state": "running",
|
||||||
|
| "current": 50,
|
||||||
|
| "target": 100,
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimProgress("uncountable", map[string]any{
|
||||||
|
| "state": "running",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package progress
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
obrimProgressTypeCountable = "countable"
|
||||||
|
obrimProgressTypeUncountable = "uncountable"
|
||||||
|
|
||||||
|
obrimProgressStateStarted = "started"
|
||||||
|
obrimProgressStateRunning = "running"
|
||||||
|
obrimProgressStateCompleted = "completed"
|
||||||
|
obrimProgressStateCanceled = "canceled"
|
||||||
|
|
||||||
|
obrimProgressSuccessStarted = "SUCCESS_STARTED"
|
||||||
|
obrimProgressSuccessRunning = "SUCCESS_RUNNING"
|
||||||
|
obrimProgressSuccessCompleted = "SUCCESS_COMPLETED"
|
||||||
|
obrimProgressSuccessCanceled = "SUCCESS_CANCELED"
|
||||||
|
|
||||||
|
obrimProgressFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimProgressFailureMissingState = "FAILURE_MISSING_STATE"
|
||||||
|
obrimProgressFailureInvalidState = "FAILURE_INVALID_STATE"
|
||||||
|
obrimProgressFailureMissingCurrent = "FAILURE_MISSING_CURRENT"
|
||||||
|
obrimProgressFailureInvalidCurrent = "FAILURE_INVALID_CURRENT"
|
||||||
|
obrimProgressFailureMissingTarget = "FAILURE_MISSING_TARGET"
|
||||||
|
obrimProgressFailureInvalidTarget = "FAILURE_INVALID_TARGET"
|
||||||
|
obrimProgressFailureNonPositiveTarget = "FAILURE_NON_POSITIVE_TARGET"
|
||||||
|
)
|
||||||
|
|
||||||
|
type obrimProgressPayload struct {
|
||||||
|
Type string
|
||||||
|
State string
|
||||||
|
Current any
|
||||||
|
Target any
|
||||||
|
Percentage any
|
||||||
|
}
|
||||||
|
|
||||||
|
type obrimProgressOutput struct {
|
||||||
|
Status bool
|
||||||
|
Code string
|
||||||
|
Payload any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimProgress manages lifecycle-aware countable and uncountable progress.
|
||||||
|
func ObrimProgress(progressType string, config map[string]any) map[string]any {
|
||||||
|
if code := obrimProgressValidateInput(progressType, config); code != "" {
|
||||||
|
return obrimProgressBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimProgressRouteRequest(progressType, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimProgressValidateInput validates the requested progress type and configuration.
|
||||||
|
func obrimProgressValidateInput(progressType string, config map[string]any) string {
|
||||||
|
switch progressType {
|
||||||
|
case obrimProgressTypeCountable:
|
||||||
|
state, exists := config["state"]
|
||||||
|
if !exists {
|
||||||
|
return obrimProgressFailureMissingState
|
||||||
|
}
|
||||||
|
|
||||||
|
stateValue, ok := state.(string)
|
||||||
|
if !ok {
|
||||||
|
return obrimProgressFailureInvalidState
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stateValue {
|
||||||
|
case obrimProgressStateStarted,
|
||||||
|
obrimProgressStateRunning,
|
||||||
|
obrimProgressStateCompleted,
|
||||||
|
obrimProgressStateCanceled:
|
||||||
|
default:
|
||||||
|
return obrimProgressFailureInvalidState
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := config["current"]; !exists {
|
||||||
|
return obrimProgressFailureMissingCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config["current"].(type) {
|
||||||
|
case int, int8, int16, int32, int64,
|
||||||
|
uint, uint8, uint16, uint32, uint64,
|
||||||
|
float32, float64:
|
||||||
|
default:
|
||||||
|
return obrimProgressFailureInvalidCurrent
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := config["target"]; !exists {
|
||||||
|
return obrimProgressFailureMissingTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config["target"].(type) {
|
||||||
|
case int, int8, int16, int32, int64,
|
||||||
|
uint, uint8, uint16, uint32, uint64,
|
||||||
|
float32, float64:
|
||||||
|
default:
|
||||||
|
return obrimProgressFailureInvalidTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
target := 0.0
|
||||||
|
|
||||||
|
switch value := config["target"].(type) {
|
||||||
|
case int:
|
||||||
|
target = float64(value)
|
||||||
|
case int8:
|
||||||
|
target = float64(value)
|
||||||
|
case int16:
|
||||||
|
target = float64(value)
|
||||||
|
case int32:
|
||||||
|
target = float64(value)
|
||||||
|
case int64:
|
||||||
|
target = float64(value)
|
||||||
|
case uint:
|
||||||
|
target = float64(value)
|
||||||
|
case uint8:
|
||||||
|
target = float64(value)
|
||||||
|
case uint16:
|
||||||
|
target = float64(value)
|
||||||
|
case uint32:
|
||||||
|
target = float64(value)
|
||||||
|
case uint64:
|
||||||
|
target = float64(value)
|
||||||
|
case float32:
|
||||||
|
target = float64(value)
|
||||||
|
case float64:
|
||||||
|
target = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if target <= 0 {
|
||||||
|
return obrimProgressFailureNonPositiveTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
case obrimProgressTypeUncountable:
|
||||||
|
state, exists := config["state"]
|
||||||
|
if !exists {
|
||||||
|
return obrimProgressFailureMissingState
|
||||||
|
}
|
||||||
|
|
||||||
|
stateValue, ok := state.(string)
|
||||||
|
if !ok {
|
||||||
|
return obrimProgressFailureInvalidState
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stateValue {
|
||||||
|
case obrimProgressStateStarted,
|
||||||
|
obrimProgressStateRunning,
|
||||||
|
obrimProgressStateCompleted,
|
||||||
|
obrimProgressStateCanceled:
|
||||||
|
default:
|
||||||
|
return obrimProgressFailureInvalidState
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return obrimProgressFailureInvalidType
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimProgressRouteRequest routes progress processing to its type-specific entry function.
|
||||||
|
func obrimProgressRouteRequest(progressType string, config map[string]any) map[string]any {
|
||||||
|
switch progressType {
|
||||||
|
case obrimProgressTypeCountable:
|
||||||
|
return obrimProgressCountable(config)
|
||||||
|
case obrimProgressTypeUncountable:
|
||||||
|
return obrimProgressUncountable(config)
|
||||||
|
default:
|
||||||
|
return obrimProgressBuildOutput(false, obrimProgressFailureInvalidType, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimProgressBuildOutput builds the standardized progress utility output.
|
||||||
|
func obrimProgressBuildOutput(status bool, code string, payload *obrimProgressPayload) map[string]any {
|
||||||
|
output := map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"code": code,
|
||||||
|
"payload": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload == nil {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
output["payload"] = map[string]any{
|
||||||
|
"type": payload.Type,
|
||||||
|
"state": payload.State,
|
||||||
|
"current": payload.Current,
|
||||||
|
"target": payload.Target,
|
||||||
|
"percentage": payload.Percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimProgressCountable processes countable progress workflows.
|
||||||
|
func obrimProgressCountable(config map[string]any) map[string]any {
|
||||||
|
state := config["state"].(string)
|
||||||
|
|
||||||
|
var current float64
|
||||||
|
var target float64
|
||||||
|
|
||||||
|
switch value := config["current"].(type) {
|
||||||
|
case int:
|
||||||
|
current = float64(value)
|
||||||
|
case int8:
|
||||||
|
current = float64(value)
|
||||||
|
case int16:
|
||||||
|
current = float64(value)
|
||||||
|
case int32:
|
||||||
|
current = float64(value)
|
||||||
|
case int64:
|
||||||
|
current = float64(value)
|
||||||
|
case uint:
|
||||||
|
current = float64(value)
|
||||||
|
case uint8:
|
||||||
|
current = float64(value)
|
||||||
|
case uint16:
|
||||||
|
current = float64(value)
|
||||||
|
case uint32:
|
||||||
|
current = float64(value)
|
||||||
|
case uint64:
|
||||||
|
current = float64(value)
|
||||||
|
case float32:
|
||||||
|
current = float64(value)
|
||||||
|
case float64:
|
||||||
|
current = value
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := config["target"].(type) {
|
||||||
|
case int:
|
||||||
|
target = float64(value)
|
||||||
|
case int8:
|
||||||
|
target = float64(value)
|
||||||
|
case int16:
|
||||||
|
target = float64(value)
|
||||||
|
case int32:
|
||||||
|
target = float64(value)
|
||||||
|
case int64:
|
||||||
|
target = float64(value)
|
||||||
|
case uint:
|
||||||
|
target = float64(value)
|
||||||
|
case uint8:
|
||||||
|
target = float64(value)
|
||||||
|
case uint16:
|
||||||
|
target = float64(value)
|
||||||
|
case uint32:
|
||||||
|
target = float64(value)
|
||||||
|
case uint64:
|
||||||
|
target = float64(value)
|
||||||
|
case float32:
|
||||||
|
target = float64(value)
|
||||||
|
case float64:
|
||||||
|
target = value
|
||||||
|
}
|
||||||
|
|
||||||
|
percentage := int(math.Round((current / target) * 100))
|
||||||
|
|
||||||
|
if percentage < 0 {
|
||||||
|
percentage = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if percentage > 100 {
|
||||||
|
percentage = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
code := obrimProgressSuccessRunning
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case obrimProgressStateStarted:
|
||||||
|
code = obrimProgressSuccessStarted
|
||||||
|
case obrimProgressStateRunning:
|
||||||
|
code = obrimProgressSuccessRunning
|
||||||
|
case obrimProgressStateCompleted:
|
||||||
|
code = obrimProgressSuccessCompleted
|
||||||
|
case obrimProgressStateCanceled:
|
||||||
|
code = obrimProgressSuccessCanceled
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimProgressBuildOutput(
|
||||||
|
true,
|
||||||
|
code,
|
||||||
|
&obrimProgressPayload{
|
||||||
|
Type: obrimProgressTypeCountable,
|
||||||
|
State: state,
|
||||||
|
Current: current,
|
||||||
|
Target: target,
|
||||||
|
Percentage: percentage,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimProgressUncountable processes uncountable progress workflows.
|
||||||
|
func obrimProgressUncountable(config map[string]any) map[string]any {
|
||||||
|
state := config["state"].(string)
|
||||||
|
|
||||||
|
code := obrimProgressSuccessRunning
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case obrimProgressStateStarted:
|
||||||
|
code = obrimProgressSuccessStarted
|
||||||
|
case obrimProgressStateRunning:
|
||||||
|
code = obrimProgressSuccessRunning
|
||||||
|
case obrimProgressStateCompleted:
|
||||||
|
code = obrimProgressSuccessCompleted
|
||||||
|
case obrimProgressStateCanceled:
|
||||||
|
code = obrimProgressSuccessCanceled
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimProgressBuildOutput(
|
||||||
|
true,
|
||||||
|
code,
|
||||||
|
&obrimProgressPayload{
|
||||||
|
Type: obrimProgressTypeUncountable,
|
||||||
|
State: state,
|
||||||
|
Current: nil,
|
||||||
|
Target: nil,
|
||||||
|
Percentage: nil,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Retriever
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Retrieve data from supported resource handlers through a unified
|
||||||
|
| retrieval interface.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Use for retrieving data from registered JSON resource handlers.
|
||||||
|
| - Use type-based dispatching to route retrieval requests.
|
||||||
|
| - Use resource retrieval to obtain a complete JSON resource.
|
||||||
|
| - Use path retrieval to obtain a specific nested value.
|
||||||
|
| - Use fields retrieval to obtain multiple nested values.
|
||||||
|
| - Do not use path and fields retrieval together.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimRetriever("json", map[string]any{
|
||||||
|
| "resource": "metadata",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimRetriever("json", map[string]any{
|
||||||
|
| "resource": "metadata",
|
||||||
|
| "path": "app.name",
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
| - ObrimRetriever("json", map[string]any{
|
||||||
|
| "resource": "metadata",
|
||||||
|
| "fields": []any{"app.name", "app.version"},
|
||||||
|
| })
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package retriever
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Utility type identifiers define supported retriever input types.
|
||||||
|
obrimRetrieverTypeJSON = "json"
|
||||||
|
|
||||||
|
// Retriever result codes define successful execution outcomes.
|
||||||
|
obrimRetrieverSuccessResource = "SUCCESS_RESOURCE_RETRIEVED"
|
||||||
|
obrimRetrieverSuccessPath = "SUCCESS_PATH_RETRIEVED"
|
||||||
|
obrimRetrieverSuccessFields = "SUCCESS_FIELDS_RETRIEVED"
|
||||||
|
|
||||||
|
// Retriever result codes define failed execution outcomes.
|
||||||
|
obrimRetrieverFailureInvalidType = "FAILURE_INVALID_TYPE"
|
||||||
|
obrimRetrieverFailureInvalidConfig = "FAILURE_INVALID_CONFIG"
|
||||||
|
obrimRetrieverFailureResourceRequired = "FAILURE_RESOURCE_REQUIRED"
|
||||||
|
obrimRetrieverFailurePathAndFields = "FAILURE_PATH_AND_FIELDS"
|
||||||
|
obrimRetrieverFailureInvalidPath = "FAILURE_INVALID_PATH"
|
||||||
|
obrimRetrieverFailureInvalidFields = "FAILURE_INVALID_FIELDS"
|
||||||
|
obrimRetrieverFailureResourceNotFound = "FAILURE_RESOURCE_NOT_FOUND"
|
||||||
|
obrimRetrieverFailureHandlerFailed = "FAILURE_HANDLER_FAILED"
|
||||||
|
obrimRetrieverFailurePathNotFound = "FAILURE_PATH_NOT_FOUND"
|
||||||
|
obrimRetrieverFailureFieldNotFound = "FAILURE_FIELD_NOT_FOUND"
|
||||||
|
obrimRetrieverFailureUnsupportedConfig = "FAILURE_UNSUPPORTED_CONFIG"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// obrimRetrieverJsonHandlers stores registered JSON resource handlers.
|
||||||
|
obrimRetrieverJsonHandlers = map[string]func() map[string]any{}
|
||||||
|
|
||||||
|
// obrimRetrieverJsonHandlersMutex protects the JSON handler registry.
|
||||||
|
obrimRetrieverJsonHandlersMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObrimRetrieverResult represents the standardized retriever execution output.
|
||||||
|
type ObrimRetrieverResult struct {
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Payload map[string]any `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimRetriever retrieves data from a supported resource handler.
|
||||||
|
func ObrimRetriever(
|
||||||
|
typ string,
|
||||||
|
config map[string]any,
|
||||||
|
) map[string]any {
|
||||||
|
if code := obrimRetrieverValidateInput(typ, config); code != "" {
|
||||||
|
return obrimRetrieverBuildOutput(false, code, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimRetrieverRouteRequest(typ, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObrimRetrieverJsonRegister registers a JSON resource handler.
|
||||||
|
func ObrimRetrieverJsonRegister(
|
||||||
|
resource string,
|
||||||
|
handler func() map[string]any,
|
||||||
|
) {
|
||||||
|
if strings.TrimSpace(resource) == "" || handler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obrimRetrieverJsonHandlersMutex.Lock()
|
||||||
|
defer obrimRetrieverJsonHandlersMutex.Unlock()
|
||||||
|
|
||||||
|
obrimRetrieverJsonHandlers[resource] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverValidateInput validates the retriever type and configuration.
|
||||||
|
func obrimRetrieverValidateInput(
|
||||||
|
typ string,
|
||||||
|
config map[string]any,
|
||||||
|
) string {
|
||||||
|
switch typ {
|
||||||
|
case obrimRetrieverTypeJSON:
|
||||||
|
return obrimRetrieverValidateJSONConfig(config)
|
||||||
|
default:
|
||||||
|
return obrimRetrieverFailureInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverValidateJSONConfig validates JSON retriever configuration.
|
||||||
|
func obrimRetrieverValidateJSONConfig(config map[string]any) string {
|
||||||
|
if config == nil {
|
||||||
|
return obrimRetrieverFailureInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceValue, exists := config["resource"]
|
||||||
|
if !exists {
|
||||||
|
return obrimRetrieverFailureResourceRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, ok := resourceValue.(string)
|
||||||
|
if !ok || strings.TrimSpace(resource) == "" {
|
||||||
|
return obrimRetrieverFailureResourceRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasPath := config["path"]
|
||||||
|
_, hasFields := config["fields"]
|
||||||
|
|
||||||
|
if hasPath && hasFields {
|
||||||
|
return obrimRetrieverFailurePathAndFields
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range config {
|
||||||
|
switch key {
|
||||||
|
case "resource", "path", "fields":
|
||||||
|
default:
|
||||||
|
return obrimRetrieverFailureUnsupportedConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPath {
|
||||||
|
path, ok := config["path"].(string)
|
||||||
|
if !ok || strings.TrimSpace(path) == "" {
|
||||||
|
return obrimRetrieverFailureInvalidPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasFields {
|
||||||
|
fields, ok := config["fields"].([]string)
|
||||||
|
if !ok || len(fields) == 0 {
|
||||||
|
return obrimRetrieverFailureInvalidFields
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
if strings.TrimSpace(field) == "" {
|
||||||
|
return obrimRetrieverFailureInvalidFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverRouteRequest routes the request to its type-specific entry function.
|
||||||
|
func obrimRetrieverRouteRequest(
|
||||||
|
typ string,
|
||||||
|
config map[string]any,
|
||||||
|
) map[string]any {
|
||||||
|
switch typ {
|
||||||
|
case obrimRetrieverTypeJSON:
|
||||||
|
return obrimRetrieverJson(config)
|
||||||
|
default:
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
false,
|
||||||
|
obrimRetrieverFailureInvalidType,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverBuildOutput builds the standardized retriever output.
|
||||||
|
func obrimRetrieverBuildOutput(
|
||||||
|
status bool,
|
||||||
|
code string,
|
||||||
|
payload map[string]any,
|
||||||
|
) map[string]any {
|
||||||
|
if !status {
|
||||||
|
payload = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"code": code,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverJson processes a JSON retrieval request.
|
||||||
|
func obrimRetrieverJson(config map[string]any) map[string]any {
|
||||||
|
resource := config["resource"].(string)
|
||||||
|
|
||||||
|
data, ok := obrimRetrieverJsonHandler(resource)
|
||||||
|
if !ok {
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
false,
|
||||||
|
obrimRetrieverFailureResourceNotFound,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathValue, exists := config["path"]; exists {
|
||||||
|
path := pathValue.(string)
|
||||||
|
|
||||||
|
value, ok := obrimRetrieverJsonPath(data, path)
|
||||||
|
if !ok {
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
false,
|
||||||
|
obrimRetrieverFailurePathNotFound,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimRetrieverSuccessPath,
|
||||||
|
map[string]any{
|
||||||
|
"data": value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldsValue, exists := config["fields"]; exists {
|
||||||
|
fields := fieldsValue.([]string)
|
||||||
|
|
||||||
|
values, ok := obrimRetrieverJsonFields(data, fields)
|
||||||
|
if !ok {
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
false,
|
||||||
|
obrimRetrieverFailureFieldNotFound,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimRetrieverSuccessFields,
|
||||||
|
map[string]any{
|
||||||
|
"data": values,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obrimRetrieverBuildOutput(
|
||||||
|
true,
|
||||||
|
obrimRetrieverSuccessResource,
|
||||||
|
map[string]any{
|
||||||
|
"data": obrimRetrieverJsonResource(data),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverJsonHandler locates and invokes a registered JSON handler.
|
||||||
|
func obrimRetrieverJsonHandler(
|
||||||
|
resource string,
|
||||||
|
) (map[string]any, bool) {
|
||||||
|
obrimRetrieverJsonHandlersMutex.RLock()
|
||||||
|
handler, exists := obrimRetrieverJsonHandlers[resource]
|
||||||
|
obrimRetrieverJsonHandlersMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists || handler == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
data := handler()
|
||||||
|
if data == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverJsonResource returns the complete JSON resource payload.
|
||||||
|
func obrimRetrieverJsonResource(
|
||||||
|
resource map[string]any,
|
||||||
|
) map[string]any {
|
||||||
|
return resource
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverJsonPath retrieves a value using a dot-notation path.
|
||||||
|
func obrimRetrieverJsonPath(
|
||||||
|
resource map[string]any,
|
||||||
|
path string,
|
||||||
|
) (any, bool) {
|
||||||
|
parts := strings.Split(path, ".")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var current any = resource
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := current.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
next, exists := value[part]
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
current = next
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
index := -1
|
||||||
|
|
||||||
|
for i, item := range value {
|
||||||
|
if part == strings.TrimSpace(part) {
|
||||||
|
var parsed int
|
||||||
|
if _, err := fmt.Sscanf(part, "%d", &parsed); err == nil {
|
||||||
|
index = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if index >= 0 {
|
||||||
|
_ = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if index < 0 || index >= len(value) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
current = value[index]
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimRetrieverJsonFields retrieves multiple values using field selection.
|
||||||
|
func obrimRetrieverJsonFields(
|
||||||
|
resource map[string]any,
|
||||||
|
fields []string,
|
||||||
|
) (map[string]any, bool) {
|
||||||
|
values := make(map[string]any, len(fields))
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
value, ok := obrimRetrieverJsonPath(resource, field)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
values[field] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, true
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Description
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Name:
|
||||||
|
| - Status
|
||||||
|
|
|
||||||
|
| Purpose:
|
||||||
|
| - Provide a framework-level CLI status utility that standardizes
|
||||||
|
| terminal message presentation using semantic labels and visual
|
||||||
|
| indicators to ensure consistent output across all softwares.
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Instruction
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Guideline:
|
||||||
|
| - Accept a semantic status category and human-readable message.
|
||||||
|
| - Use INFO, WARNING, SUCCESS, and ERROR as supported status labels.
|
||||||
|
| - Treat unsupported, empty, or invalid labels as INFO.
|
||||||
|
| - Emit exactly one standardized terminal message per invocation.
|
||||||
|
|
|
||||||
|
| Example:
|
||||||
|
| - ObrimStatus(
|
||||||
|
| "INFO",
|
||||||
|
| "Application started successfully.",
|
||||||
|
| )
|
||||||
|
|
|
||||||
|
| - ObrimStatus(
|
||||||
|
| "WARNING",
|
||||||
|
| "Configuration file not found.",
|
||||||
|
| )
|
||||||
|
|
|
||||||
|
| - ObrimStatus(
|
||||||
|
| "SUCCESS",
|
||||||
|
| "Installation completed.",
|
||||||
|
| )
|
||||||
|
|
|
||||||
|
| - ObrimStatus(
|
||||||
|
| "ERROR",
|
||||||
|
| "Unable to connect to the server.",
|
||||||
|
| )
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Credit
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Contributor:
|
||||||
|
| - Rajon Ahmed
|
||||||
|
| - Blockonite
|
||||||
|
|
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObrimStatus emits a standardized terminal message.
|
||||||
|
func ObrimStatus(label, message string) {
|
||||||
|
normalizedLabel, normalizedMessage := obrimStatusValidateInput(label, message)
|
||||||
|
output := obrimStatusBuildOutput(obrimStatusFormatMessage(normalizedLabel, normalizedMessage))
|
||||||
|
|
||||||
|
fmt.Print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimStatusValidateInput normalizes and validates the status label and message.
|
||||||
|
func obrimStatusValidateInput(label, message string) (string, string) {
|
||||||
|
normalizedLabel := strings.ToUpper(strings.TrimSpace(label))
|
||||||
|
normalizedMessage := strings.TrimSpace(message)
|
||||||
|
|
||||||
|
switch normalizedLabel {
|
||||||
|
case "INFO", "WARNING", "SUCCESS", "ERROR":
|
||||||
|
default:
|
||||||
|
normalizedLabel = "INFO"
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedLabel, normalizedMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimStatusFormatMessage formats the status label and message with its visual indicator.
|
||||||
|
func obrimStatusFormatMessage(label, message string) string {
|
||||||
|
var indicator string
|
||||||
|
|
||||||
|
switch label {
|
||||||
|
case "WARNING":
|
||||||
|
indicator = "!"
|
||||||
|
case "SUCCESS":
|
||||||
|
indicator = "✓"
|
||||||
|
case "ERROR":
|
||||||
|
indicator = "✗"
|
||||||
|
default:
|
||||||
|
indicator = "ℹ"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%s %s] %s\n", indicator, label, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obrimStatusBuildOutput builds the final terminal message.
|
||||||
|
func obrimStatusBuildOutput(message string) string {
|
||||||
|
return message
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user