feat: 新增XTS/CCM流式与KDF能力,补充安全测试并更新README/CHANGELOG
This commit is contained in:
+59
-7
@@ -2,10 +2,11 @@ package filex
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidSplitPattern = errors.New("split dst pattern must contain exactly one '*'")
|
||||
|
||||
func Attach(src, dst, output string) error {
|
||||
fpsrc, err := os.Open(src)
|
||||
if err != nil {
|
||||
@@ -81,6 +84,9 @@ func SplitFile(src, dst string, num int, bynum bool, progress func(float64)) err
|
||||
if num <= 0 {
|
||||
return errors.New("num must be greater than zero")
|
||||
}
|
||||
if strings.Count(dst, "*") != 1 {
|
||||
return ErrInvalidSplitPattern
|
||||
}
|
||||
|
||||
fpsrc, err := os.Open(src)
|
||||
if err != nil {
|
||||
@@ -244,6 +250,8 @@ func MergeFile(src, dst string, progress func(float64)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FillWithRandom fills file with pseudo-random bytes generated by math/rand.
|
||||
// It is fast but not cryptographically secure.
|
||||
func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(float64)) error {
|
||||
if filesize < 0 {
|
||||
return errors.New("filesize must be non-negative")
|
||||
@@ -258,7 +266,7 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
|
||||
bufcap = filesize
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
r := mrand.New(mrand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
fp, err := os.Create(path)
|
||||
if err != nil {
|
||||
@@ -267,18 +275,17 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
|
||||
defer fp.Close()
|
||||
|
||||
writer := bufio.NewWriter(fp)
|
||||
defer writer.Flush()
|
||||
|
||||
if filesize == 0 {
|
||||
reportProgress(progress, 0, 0)
|
||||
return nil
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
pool := make([][]byte, 0, bufnum)
|
||||
for i := 0; i < bufnum; i++ {
|
||||
b := make([]byte, bufcap)
|
||||
for j := 0; j < bufcap; j++ {
|
||||
b[j] = byte(rand.Intn(256))
|
||||
b[j] = byte(r.Intn(256))
|
||||
}
|
||||
pool = append(pool, b)
|
||||
}
|
||||
@@ -289,14 +296,59 @@ func FillWithRandom(path string, filesize, bufcap, bufnum int, progress func(flo
|
||||
if filesize-written < chunk {
|
||||
chunk = filesize - written
|
||||
}
|
||||
buf := pool[rand.Intn(len(pool))][:chunk]
|
||||
buf := pool[r.Intn(len(pool))][:chunk]
|
||||
if _, err := writer.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
written += chunk
|
||||
reportProgress(progress, int64(written), int64(filesize))
|
||||
}
|
||||
return nil
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
// FillWithCryptoRandom fills file with cryptographically secure random bytes from crypto/rand.
|
||||
// Security is stronger than FillWithRandom, but throughput may be lower.
|
||||
func FillWithCryptoRandom(path string, filesize, bufcap int, progress func(float64)) error {
|
||||
if filesize < 0 {
|
||||
return errors.New("filesize must be non-negative")
|
||||
}
|
||||
if bufcap <= 0 {
|
||||
bufcap = 1
|
||||
}
|
||||
if bufcap > filesize && filesize > 0 {
|
||||
bufcap = filesize
|
||||
}
|
||||
|
||||
fp, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
writer := bufio.NewWriter(fp)
|
||||
|
||||
if filesize == 0 {
|
||||
reportProgress(progress, 0, 0)
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
buf := make([]byte, bufcap)
|
||||
written := 0
|
||||
for written < filesize {
|
||||
chunk := bufcap
|
||||
if filesize-written < chunk {
|
||||
chunk = filesize - written
|
||||
}
|
||||
if _, err := io.ReadFull(crand.Reader, buf[:chunk]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write(buf[:chunk]); err != nil {
|
||||
return err
|
||||
}
|
||||
written += chunk
|
||||
reportProgress(progress, int64(written), int64(filesize))
|
||||
}
|
||||
return writer.Flush()
|
||||
}
|
||||
|
||||
func reportProgress(progress func(float64), current, total int64) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package filex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFillWithRandomAndCryptoRandom(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
pseudoPath := filepath.Join(dir, "pseudo.bin")
|
||||
securePath := filepath.Join(dir, "secure.bin")
|
||||
|
||||
if err := FillWithRandom(pseudoPath, 2048, 128, 4, nil); err != nil {
|
||||
t.Fatalf("FillWithRandom failed: %v", err)
|
||||
}
|
||||
if err := FillWithCryptoRandom(securePath, 2048, 128, nil); err != nil {
|
||||
t.Fatalf("FillWithCryptoRandom failed: %v", err)
|
||||
}
|
||||
|
||||
pseudoInfo, err := os.Stat(pseudoPath)
|
||||
if err != nil {
|
||||
t.Fatalf("stat pseudo file failed: %v", err)
|
||||
}
|
||||
if pseudoInfo.Size() != 2048 {
|
||||
t.Fatalf("unexpected pseudo size: %d", pseudoInfo.Size())
|
||||
}
|
||||
|
||||
secureInfo, err := os.Stat(securePath)
|
||||
if err != nil {
|
||||
t.Fatalf("stat secure file failed: %v", err)
|
||||
}
|
||||
if secureInfo.Size() != 2048 {
|
||||
t.Fatalf("unexpected secure size: %d", secureInfo.Size())
|
||||
}
|
||||
|
||||
pseudo, err := os.ReadFile(pseudoPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read pseudo file failed: %v", err)
|
||||
}
|
||||
secure, err := os.ReadFile(securePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read secure file failed: %v", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(secure, make([]byte, len(secure))) {
|
||||
t.Fatalf("secure random output should not be all zero")
|
||||
}
|
||||
if bytes.Equal(pseudo, secure) {
|
||||
t.Fatalf("pseudo and secure random outputs unexpectedly identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillWithRandomInvalidArgs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "bad.bin")
|
||||
|
||||
if err := FillWithRandom(path, -1, 16, 1, nil); err == nil {
|
||||
t.Fatalf("expected FillWithRandom negative filesize error")
|
||||
}
|
||||
if err := FillWithCryptoRandom(path, -1, 16, nil); err == nil {
|
||||
t.Fatalf("expected FillWithCryptoRandom negative filesize error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package filex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitFilePatternValidation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "src.bin")
|
||||
if err := os.WriteFile(src, bytes.Repeat([]byte{0x7f}, 64), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
|
||||
if err := SplitFile(src, filepath.Join(dir, "part.bin"), 2, true, nil); !errors.Is(err, ErrInvalidSplitPattern) {
|
||||
t.Fatalf("expected ErrInvalidSplitPattern for missing '*', got: %v", err)
|
||||
}
|
||||
if err := SplitFile(src, filepath.Join(dir, "part_*_*.bin"), 2, true, nil); !errors.Is(err, ErrInvalidSplitPattern) {
|
||||
t.Fatalf("expected ErrInvalidSplitPattern for multiple '*', got: %v", err)
|
||||
}
|
||||
|
||||
pattern := filepath.Join(dir, "part_*.bin")
|
||||
if err := SplitFile(src, pattern, 2, true, nil); err != nil {
|
||||
t.Fatalf("SplitFile valid pattern failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, "part_0.bin")); err != nil {
|
||||
t.Fatalf("part_0.bin not found: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "part_1.bin")); err != nil {
|
||||
t.Fatalf("part_1.bin not found: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user