Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
c492c80881
|
|||
26dd16d021
|
|||
b0b43de8ca
|
|||
94f72e4ddf
|
|||
df4388e6dc
|
|||
fd33b43f31
|
|||
be4de07eb8
|
|||
36ed474bfe
|
|||
fdc590c8c3
|
|||
1990e5d32d
|
|||
72883cf6bd
|
@@ -1,8 +1,8 @@
|
||||
package cmdext
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
@@ -30,119 +30,41 @@ func run(opt CommandRunner) (CommandResult, error) {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
|
||||
preader := pipeReader{
|
||||
stdout: stdoutPipe,
|
||||
stderr: stderrPipe,
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
|
||||
errch := make(chan error, 3)
|
||||
go func() { errch <- cmd.Wait() }()
|
||||
type resultObj struct {
|
||||
stdout string
|
||||
stderr string
|
||||
stdcombined string
|
||||
err error
|
||||
}
|
||||
|
||||
// [1] read raw stdout
|
||||
|
||||
stdoutBufferReader, stdoutBufferWriter := io.Pipe()
|
||||
stdout := ""
|
||||
outputChan := make(chan resultObj)
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for true {
|
||||
n, out := stdoutPipe.Read(buf)
|
||||
// we need to first fully read the pipes and then call Wait
|
||||
// see https://pkg.go.dev/os/exec#Cmd.StdoutPipe
|
||||
|
||||
if n > 0 {
|
||||
txt := string(buf[:n])
|
||||
stdout += txt
|
||||
_, _ = stdoutBufferWriter.Write(buf[:n])
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.ReadRawStdout(buf[:n])
|
||||
}
|
||||
}
|
||||
if out == io.EOF {
|
||||
break
|
||||
}
|
||||
if out != nil {
|
||||
errch <- out
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
stdout, stderr, stdcombined, err := preader.Read(opt.listener)
|
||||
if err != nil {
|
||||
outputChan <- resultObj{stdout, stderr, stdcombined, err}
|
||||
}
|
||||
_ = stdoutBufferWriter.Close()
|
||||
}()
|
||||
|
||||
// [2] read raw stderr
|
||||
|
||||
stderrBufferReader, stderrBufferWriter := io.Pipe()
|
||||
stderr := ""
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for true {
|
||||
n, err := stderrPipe.Read(buf)
|
||||
|
||||
if n > 0 {
|
||||
txt := string(buf[:n])
|
||||
stderr += txt
|
||||
_, _ = stderrBufferWriter.Write(buf[:n])
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.ReadRawStderr(buf[:n])
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
errch <- err
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
outputChan <- resultObj{stdout, stderr, stdcombined, err}
|
||||
}
|
||||
_ = stderrBufferWriter.Close()
|
||||
|
||||
outputChan <- resultObj{stdout, stderr, stdcombined, nil}
|
||||
}()
|
||||
|
||||
combch := make(chan string, 32)
|
||||
stopCombch := make(chan bool)
|
||||
|
||||
// [3] collect stdout line-by-line
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdoutBufferReader)
|
||||
for scanner.Scan() {
|
||||
txt := scanner.Text()
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.ReadStdoutLine(txt)
|
||||
}
|
||||
combch <- txt
|
||||
}
|
||||
}()
|
||||
|
||||
// [4] collect stderr line-by-line
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderrBufferReader)
|
||||
for scanner.Scan() {
|
||||
txt := scanner.Text()
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.ReadStderrLine(txt)
|
||||
}
|
||||
combch <- txt
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() { stopCombch <- true }()
|
||||
|
||||
// [5] combine stdcombined
|
||||
|
||||
stdcombined := ""
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case txt := <-combch:
|
||||
stdcombined += txt + "\n" // this comes from bufio.Scanner and has no newlines...
|
||||
case <-stopCombch:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// [6] run
|
||||
|
||||
var timeoutChan <-chan time.Time = make(chan time.Time, 1)
|
||||
if opt.timeout != nil {
|
||||
timeoutChan = time.After(*opt.timeout)
|
||||
@@ -155,24 +77,37 @@ func run(opt CommandRunner) (CommandResult, error) {
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.Timeout()
|
||||
}
|
||||
return CommandResult{
|
||||
StdOut: stdout,
|
||||
StdErr: stderr,
|
||||
StdCombined: stdcombined,
|
||||
ExitCode: -1,
|
||||
CommandTimedOut: true,
|
||||
}, nil
|
||||
|
||||
case err := <-errch:
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
if fallback, ok := syncext.ReadChannelWithTimeout(outputChan, mathext.Min(32*time.Millisecond, *opt.timeout)); ok {
|
||||
// most of the time the cmd.Process.Kill() should also ahve finished the pipereader
|
||||
// and we can at least return the already collected stdout, stderr, etc
|
||||
return CommandResult{
|
||||
StdOut: fallback.stdout,
|
||||
StdErr: fallback.stderr,
|
||||
StdCombined: fallback.stdcombined,
|
||||
ExitCode: -1,
|
||||
CommandTimedOut: true,
|
||||
}, nil
|
||||
} else {
|
||||
return CommandResult{
|
||||
StdOut: "",
|
||||
StdErr: "",
|
||||
StdCombined: "",
|
||||
ExitCode: -1,
|
||||
CommandTimedOut: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
case outobj := <-outputChan:
|
||||
if exiterr, ok := outobj.err.(*exec.ExitError); ok {
|
||||
excode := exiterr.ExitCode()
|
||||
for _, lstr := range opt.listener {
|
||||
lstr.Finished(excode)
|
||||
}
|
||||
return CommandResult{
|
||||
StdOut: stdout,
|
||||
StdErr: stderr,
|
||||
StdCombined: stdcombined,
|
||||
StdOut: outobj.stdout,
|
||||
StdErr: outobj.stderr,
|
||||
StdCombined: outobj.stdcombined,
|
||||
ExitCode: excode,
|
||||
CommandTimedOut: false,
|
||||
}, nil
|
||||
@@ -183,9 +118,9 @@ func run(opt CommandRunner) (CommandResult, error) {
|
||||
lstr.Finished(0)
|
||||
}
|
||||
return CommandResult{
|
||||
StdOut: stdout,
|
||||
StdErr: stderr,
|
||||
StdCombined: stdcombined,
|
||||
StdOut: outobj.stdout,
|
||||
StdErr: outobj.stderr,
|
||||
StdCombined: outobj.stdcombined,
|
||||
ExitCode: 0,
|
||||
CommandTimedOut: false,
|
||||
}, nil
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package cmdext
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStdout(t *testing.T) {
|
||||
|
||||
@@ -57,3 +61,185 @@ func TestStdcombined(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPartialRead(t *testing.T) {
|
||||
res1, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys; import time; print(\"first message\", flush=True); time.sleep(5); print(\"cant see me\", flush=True);").
|
||||
Timeout(100 * time.Millisecond).
|
||||
Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if !res1.CommandTimedOut {
|
||||
t.Errorf("!CommandTimedOut")
|
||||
}
|
||||
if res1.StdErr != "" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "first message\n" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "first message\n" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPartialReadStderr(t *testing.T) {
|
||||
res1, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys; import time; print(\"first message\", file=sys.stderr, flush=True); time.sleep(5); print(\"cant see me\", file=sys.stderr, flush=True);").
|
||||
Timeout(100 * time.Millisecond).
|
||||
Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if !res1.CommandTimedOut {
|
||||
t.Errorf("!CommandTimedOut")
|
||||
}
|
||||
if res1.StdErr != "first message\n" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "first message\n" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReadUnflushedStdout(t *testing.T) {
|
||||
|
||||
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stdout, end='')").Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if res1.StdErr != "" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "message101" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "message101\n" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReadUnflushedStderr(t *testing.T) {
|
||||
|
||||
res1, err := Runner("python").Arg("-c").Arg("import sys; print(\"message101\", file=sys.stderr, end='')").Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if res1.StdErr != "message101" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "message101\n" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPartialReadUnflushed(t *testing.T) {
|
||||
t.SkipNow()
|
||||
|
||||
res1, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys; import time; print(\"first message\", end=''); time.sleep(5); print(\"cant see me\", end='');").
|
||||
Timeout(100 * time.Millisecond).
|
||||
Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if !res1.CommandTimedOut {
|
||||
t.Errorf("!CommandTimedOut")
|
||||
}
|
||||
if res1.StdErr != "" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "first message" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "first message" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPartialReadUnflushedStderr(t *testing.T) {
|
||||
t.SkipNow()
|
||||
|
||||
res1, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys; import time; print(\"first message\", file=sys.stderr, end=''); time.sleep(5); print(\"cant see me\", file=sys.stderr, end='');").
|
||||
Timeout(100 * time.Millisecond).
|
||||
Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if !res1.CommandTimedOut {
|
||||
t.Errorf("!CommandTimedOut")
|
||||
}
|
||||
if res1.StdErr != "first message" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if res1.StdOut != "" {
|
||||
t.Errorf("res1.StdOut == '%v'", res1.StdOut)
|
||||
}
|
||||
if res1.StdCombined != "first message" {
|
||||
t.Errorf("res1.StdCombined == '%v'", res1.StdCombined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestListener(t *testing.T) {
|
||||
|
||||
_, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys;" +
|
||||
"import time;" +
|
||||
"print(\"message 1\", flush=True);" +
|
||||
"time.sleep(1);" +
|
||||
"print(\"message 2\", flush=True);" +
|
||||
"time.sleep(1);" +
|
||||
"print(\"message 3\", flush=True);" +
|
||||
"time.sleep(1);" +
|
||||
"print(\"message 4\", file=sys.stderr, flush=True);" +
|
||||
"time.sleep(1);" +
|
||||
"print(\"message 5\", flush=True);" +
|
||||
"time.sleep(1);" +
|
||||
"print(\"final\");").
|
||||
ListenStdout(func(s string) { fmt.Printf("@@STDOUT <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
|
||||
ListenStderr(func(s string) { fmt.Printf("@@STDERR <<- %v (%v)\n", s, time.Now().Format(time.RFC3339Nano)) }).
|
||||
Timeout(10 * time.Second).
|
||||
Run()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongStdout(t *testing.T) {
|
||||
|
||||
res1, err := Runner("python").
|
||||
Arg("-c").
|
||||
Arg("import sys; import time; print(\"1234\" * 10000 + \"\\n\"); print(\"1234\" * 10000 + \"\\n\"); print(\"1234\" * 10000 + \"\\n\");").
|
||||
Timeout(100 * time.Millisecond).
|
||||
Run()
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
if res1.StdErr != "" {
|
||||
t.Errorf("res1.StdErr == '%v'", res1.StdErr)
|
||||
}
|
||||
if len(res1.StdOut) != 120006 {
|
||||
t.Errorf("len(res1.StdOut) == '%v'", len(res1.StdOut))
|
||||
}
|
||||
|
||||
}
|
||||
|
146
cmdext/pipereader.go
Normal file
146
cmdext/pipereader.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package cmdext
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/syncext"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type pipeReader struct {
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
}
|
||||
|
||||
// Read ready stdout and stdin until finished
|
||||
// also splits both pipes into lines and calld the listener
|
||||
func (pr *pipeReader) Read(listener []CommandListener) (string, string, string, error) {
|
||||
type combevt struct {
|
||||
line string
|
||||
stop bool
|
||||
}
|
||||
|
||||
errch := make(chan error, 8)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// [1] read raw stdout
|
||||
|
||||
wg.Add(1)
|
||||
stdoutBufferReader, stdoutBufferWriter := io.Pipe()
|
||||
stdout := ""
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for true {
|
||||
n, out := pr.stdout.Read(buf)
|
||||
|
||||
if n > 0 {
|
||||
txt := string(buf[:n])
|
||||
stdout += txt
|
||||
_, _ = stdoutBufferWriter.Write(buf[:n])
|
||||
for _, lstr := range listener {
|
||||
lstr.ReadRawStdout(buf[:n])
|
||||
}
|
||||
}
|
||||
if out == io.EOF {
|
||||
break
|
||||
}
|
||||
if out != nil {
|
||||
errch <- out
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = stdoutBufferWriter.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// [2] read raw stderr
|
||||
|
||||
wg.Add(1)
|
||||
stderrBufferReader, stderrBufferWriter := io.Pipe()
|
||||
stderr := ""
|
||||
go func() {
|
||||
buf := make([]byte, 128)
|
||||
for true {
|
||||
n, err := pr.stderr.Read(buf)
|
||||
|
||||
if n > 0 {
|
||||
txt := string(buf[:n])
|
||||
stderr += txt
|
||||
_, _ = stderrBufferWriter.Write(buf[:n])
|
||||
for _, lstr := range listener {
|
||||
lstr.ReadRawStderr(buf[:n])
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
errch <- err
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = stderrBufferWriter.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
combch := make(chan combevt, 32)
|
||||
|
||||
// [3] collect stdout line-by-line
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdoutBufferReader)
|
||||
for scanner.Scan() {
|
||||
txt := scanner.Text()
|
||||
for _, lstr := range listener {
|
||||
lstr.ReadStdoutLine(txt)
|
||||
}
|
||||
combch <- combevt{txt, false}
|
||||
}
|
||||
combch <- combevt{"", true}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// [4] collect stderr line-by-line
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderrBufferReader)
|
||||
for scanner.Scan() {
|
||||
txt := scanner.Text()
|
||||
for _, lstr := range listener {
|
||||
lstr.ReadStderrLine(txt)
|
||||
}
|
||||
combch <- combevt{txt, false}
|
||||
}
|
||||
combch <- combevt{"", true}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// [5] combine stdcombined
|
||||
|
||||
wg.Add(1)
|
||||
stdcombined := ""
|
||||
go func() {
|
||||
stopctr := 0
|
||||
for stopctr < 2 {
|
||||
vvv := <-combch
|
||||
if vvv.stop {
|
||||
stopctr++
|
||||
} else {
|
||||
stdcombined += vvv.line + "\n" // this comes from bufio.Scanner and has no newlines...
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// wait for all (5) goroutines to finish
|
||||
wg.Wait()
|
||||
|
||||
if err, ok := syncext.ReadNonBlocking(errch); ok {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return stdout, stderr, stdcombined, nil
|
||||
}
|
136
cryptext/aes.go
136
cryptext/aes.go
@@ -1,10 +1,13 @@
|
||||
package cryptext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"io"
|
||||
@@ -12,35 +15,90 @@ import (
|
||||
|
||||
// https://stackoverflow.com/a/18819040/1761622
|
||||
|
||||
func EncryptAESSimple(password, text []byte) ([]byte, error) {
|
||||
|
||||
key, err := scrypt.Key(password, nil, 32768, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := base64.StdEncoding.EncodeToString(text)
|
||||
ciphertext := make([]byte, aes.BlockSize+len(b))
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
||||
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
|
||||
|
||||
return ciphertext, nil
|
||||
type aesPayload struct {
|
||||
Salt []byte `json:"s"`
|
||||
IV []byte `json:"i"`
|
||||
Data []byte `json:"d"`
|
||||
Rounds int `json:"r"`
|
||||
Version uint `json:"v"`
|
||||
}
|
||||
|
||||
func DecryptAESSimple(password, text []byte) ([]byte, error) {
|
||||
func EncryptAESSimple(password []byte, data []byte, rounds int) (string, error) {
|
||||
|
||||
key, err := scrypt.Key(password, nil, 32768, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing
|
||||
salt := make([]byte, 8)
|
||||
_, err := io.ReadFull(rand.Reader, salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := scrypt.Key(password, salt, rounds, 8, 1, 32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
checksum := h.Sum(nil)
|
||||
if len(checksum) != 32 {
|
||||
return "", errors.New("wrong cs size")
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, 32+len(data))
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
_, err = io.ReadFull(rand.Reader, iv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
combinedData := make([]byte, 0, 32+len(data))
|
||||
combinedData = append(combinedData, checksum...)
|
||||
combinedData = append(combinedData, data...)
|
||||
|
||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
||||
cfb.XORKeyStream(ciphertext, combinedData)
|
||||
|
||||
pl := aesPayload{
|
||||
Salt: salt,
|
||||
IV: iv,
|
||||
Data: ciphertext,
|
||||
Version: 1,
|
||||
Rounds: rounds,
|
||||
}
|
||||
|
||||
jbin, err := json.Marshal(pl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(jbin)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DecryptAESSimple(password []byte, encText string) ([]byte, error) {
|
||||
|
||||
jbin, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pl aesPayload
|
||||
err = json.Unmarshal(jbin, &pl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pl.Version != 1 {
|
||||
return nil, errors.New("unsupported version")
|
||||
}
|
||||
|
||||
key, err := scrypt.Key(password, pl.Salt, pl.Rounds, 8, 1, 32) // this is not 100% correct, rounds too low and salt is missing
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,18 +108,24 @@ func DecryptAESSimple(password, text []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(text) < aes.BlockSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
dest := make([]byte, len(pl.Data))
|
||||
|
||||
cfb := cipher.NewCFBDecrypter(block, pl.IV)
|
||||
cfb.XORKeyStream(dest, pl.Data)
|
||||
|
||||
if len(dest) < 32 {
|
||||
return nil, errors.New("payload too small")
|
||||
}
|
||||
|
||||
iv := text[:aes.BlockSize]
|
||||
text = text[aes.BlockSize:]
|
||||
cfb := cipher.NewCFBDecrypter(block, iv)
|
||||
cfb.XORKeyStream(text, text)
|
||||
chck := dest[:32]
|
||||
data := dest[32:]
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(string(text))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
chck2 := h.Sum(nil)
|
||||
|
||||
if !bytes.Equal(chck, chck2) {
|
||||
return nil, errors.New("checksum mismatch")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package cryptext
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncryptAESSimple(t *testing.T) {
|
||||
|
||||
@@ -8,15 +11,25 @@ func TestEncryptAESSimple(t *testing.T) {
|
||||
|
||||
str1 := []byte("Hello World")
|
||||
|
||||
str2, err := EncryptAESSimple(pw, str1)
|
||||
str2, err := EncryptAESSimple(pw, str1, 512)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", str2)
|
||||
|
||||
str3, err := DecryptAESSimple(pw, str2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assertEqual(t, string(str1), string(str3))
|
||||
|
||||
str4, err := EncryptAESSimple(pw, str3, 512)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assertNotEqual(t, string(str2), string(str4))
|
||||
|
||||
}
|
||||
|
@@ -23,3 +23,9 @@ func assertEqual(t *testing.T, actual string, expected string) {
|
||||
t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNotEqual(t *testing.T, actual string, expected string) {
|
||||
if actual == expected {
|
||||
t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected)
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ func init() {
|
||||
}
|
||||
|
||||
func TestResultCache1(t *testing.T) {
|
||||
cache := NewLRUMap[string](8)
|
||||
cache := NewLRUMap[string, string](8)
|
||||
verifyLRUList(cache, t)
|
||||
|
||||
key := randomKey()
|
||||
@@ -50,7 +50,7 @@ func TestResultCache1(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResultCache2(t *testing.T) {
|
||||
cache := NewLRUMap[string](8)
|
||||
cache := NewLRUMap[string, string](8)
|
||||
verifyLRUList(cache, t)
|
||||
|
||||
key1 := "key1"
|
||||
@@ -150,7 +150,7 @@ func TestResultCache2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResultCache3(t *testing.T) {
|
||||
cache := NewLRUMap[string](8)
|
||||
cache := NewLRUMap[string, string](8)
|
||||
verifyLRUList(cache, t)
|
||||
|
||||
key1 := "key1"
|
||||
@@ -173,7 +173,7 @@ func TestResultCache3(t *testing.T) {
|
||||
}
|
||||
|
||||
// does a basic consistency check over the internal cache representation
|
||||
func verifyLRUList[TData any](cache *LRUMap[TData], t *testing.T) {
|
||||
func verifyLRUList[TKey comparable, TData any](cache *LRUMap[TKey, TData], t *testing.T) {
|
||||
size := 0
|
||||
|
||||
tailFound := false
|
||||
|
1
go.mod
1
go.mod
@@ -9,5 +9,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.11.1 // indirect
|
||||
golang.org/x/crypto v0.4.0 // indirect
|
||||
)
|
||||
|
35
go.sum
35
go.sum
@@ -1,15 +1,50 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8=
|
||||
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -265,6 +265,16 @@ func ArrMap[T1 any, T2 any](arr []T1, conv func(v T1) T2) []T2 {
|
||||
return r
|
||||
}
|
||||
|
||||
func ArrFilterMap[T1 any, T2 any](arr []T1, filter func(v T1) bool, conv func(v T1) T2) []T2 {
|
||||
r := make([]T2, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if filter(v) {
|
||||
r = append(r, conv(v))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ArrSum[T NumberConstraint](arr []T) T {
|
||||
var r T = 0
|
||||
for _, v := range arr {
|
||||
|
@@ -7,3 +7,11 @@ func MapKeyArr[T comparable, V any](v map[T]V) []T {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ArrToMap[T comparable, V any](a []V, keyfunc func(V) T) map[T]V {
|
||||
result := make(map[T]V, len(a))
|
||||
for _, v := range a {
|
||||
result[keyfunc(v)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@@ -43,3 +43,45 @@ type RFCTime interface {
|
||||
GoString() string
|
||||
String() string
|
||||
}
|
||||
|
||||
type RFCDuration interface {
|
||||
Time() time.Time
|
||||
Serialize() string
|
||||
|
||||
UnmarshalJSON(bytes []byte) error
|
||||
MarshalJSON() ([]byte, error)
|
||||
|
||||
MarshalBinary() ([]byte, error)
|
||||
UnmarshalBinary(data []byte) error
|
||||
|
||||
GobEncode() ([]byte, error)
|
||||
GobDecode(data []byte) error
|
||||
|
||||
MarshalText() ([]byte, error)
|
||||
UnmarshalText(data []byte) error
|
||||
|
||||
After(u RFCTime) bool
|
||||
Before(u RFCTime) bool
|
||||
Equal(u RFCTime) bool
|
||||
IsZero() bool
|
||||
Date() (year int, month time.Month, day int)
|
||||
Year() int
|
||||
Month() time.Month
|
||||
Day() int
|
||||
Weekday() time.Weekday
|
||||
ISOWeek() (year, week int)
|
||||
Clock() (hour, min, sec int)
|
||||
Hour() int
|
||||
Minute() int
|
||||
Second() int
|
||||
Nanosecond() int
|
||||
YearDay() int
|
||||
Sub(u RFCTime) time.Duration
|
||||
Unix() int64
|
||||
UnixMilli() int64
|
||||
UnixMicro() int64
|
||||
UnixNano() int64
|
||||
Format(layout string) string
|
||||
GoString() string
|
||||
String() string
|
||||
}
|
||||
|
@@ -2,6 +2,10 @@ package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/bsontype"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -61,6 +65,23 @@ func (t *RFC3339Time) UnmarshalText(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RFC3339Time) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
|
||||
if bt != bsontype.DateTime {
|
||||
return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt))
|
||||
}
|
||||
var tt time.Time
|
||||
err := bson.Unmarshal(data, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = RFC3339Time(tt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t RFC3339Time) MarshalBSONValue() (bsontype.Type, []byte, error) {
|
||||
return bson.MarshalValue(time.Time(t))
|
||||
}
|
||||
|
||||
func (t RFC3339Time) Serialize() string {
|
||||
return t.Time().Format(t.FormatStr())
|
||||
}
|
||||
|
@@ -2,6 +2,10 @@ package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/bsontype"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -61,6 +65,23 @@ func (t *RFC3339NanoTime) UnmarshalText(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RFC3339NanoTime) UnmarshalBSONValue(bt bsontype.Type, data []byte) error {
|
||||
if bt != bsontype.DateTime {
|
||||
return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339NanoTime", bt))
|
||||
}
|
||||
var tt time.Time
|
||||
err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = RFC3339NanoTime(tt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t RFC3339NanoTime) MarshalBSONValue() (bsontype.Type, []byte, error) {
|
||||
return bson.MarshalValue(time.Time(t))
|
||||
}
|
||||
|
||||
func (t RFC3339NanoTime) Serialize() string {
|
||||
return t.Time().Format(t.FormatStr())
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ func TestRoundtrip(t *testing.T) {
|
||||
Value RFC3339NanoTime `json:"v"`
|
||||
}
|
||||
|
||||
val1 := NewRFC3339Nano(time.Now())
|
||||
val1 := NewRFC3339Nano(time.Unix(0, 1675951556820915171))
|
||||
w1 := Wrap{val1}
|
||||
|
||||
jstr1, err := json.Marshal(w1)
|
||||
@@ -20,7 +20,8 @@ func TestRoundtrip(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if string(jstr1) != "{\"v\":\"2023-01-29T20:32:36.149692117+01:00\"}" {
|
||||
if string(jstr1) != "{\"v\":\"2023-02-09T15:05:56.820915171+01:00\"}" {
|
||||
t.Errorf(string(jstr1))
|
||||
t.Errorf("repr differs")
|
||||
}
|
||||
|
||||
|
59
rfctime/seconds.go
Normal file
59
rfctime/seconds.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package rfctime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SecondsF64 time.Duration
|
||||
|
||||
func (d SecondsF64) Duration() time.Duration {
|
||||
return time.Duration(d)
|
||||
}
|
||||
|
||||
func (d SecondsF64) String() string {
|
||||
return d.Duration().String()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Nanoseconds() int64 {
|
||||
return d.Duration().Nanoseconds()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Microseconds() int64 {
|
||||
return d.Duration().Microseconds()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Milliseconds() int64 {
|
||||
return d.Duration().Milliseconds()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Seconds() float64 {
|
||||
return d.Duration().Seconds()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Minutes() float64 {
|
||||
return d.Duration().Minutes()
|
||||
}
|
||||
|
||||
func (d SecondsF64) Hours() float64 {
|
||||
return d.Duration().Hours()
|
||||
}
|
||||
|
||||
func (d *SecondsF64) UnmarshalJSON(data []byte) error {
|
||||
var secs float64 = 0
|
||||
if err := json.Unmarshal(data, &secs); err != nil {
|
||||
return err
|
||||
}
|
||||
*d = SecondsF64(timeext.FromSeconds(secs))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d SecondsF64) MarshalJSON() ([]byte, error) {
|
||||
secs := d.Seconds()
|
||||
return json.Marshal(secs)
|
||||
}
|
||||
|
||||
func NewSecondsF64(t time.Duration) SecondsF64 {
|
||||
return SecondsF64(t)
|
||||
}
|
Reference in New Issue
Block a user