Implememt the file download seek support

Update to support more fields in RevisionXAttrCommon
This commit is contained in:
Chun-Hung Tseng
2023-07-17 21:51:04 +02:00
parent 733b3e5b6e
commit 8d7db1ea7b
7 changed files with 202 additions and 75 deletions

View File

@@ -406,3 +406,53 @@ func TestUploadLargeNumberOfBlocks(t *testing.T) {
deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false, false)
checkActiveFileListing(t, ctx, protonDrive, []string{})
}
func TestFileSeek(t *testing.T) {
ctx, cancel, protonDrive := setup(t, false)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
// in order to simulate seeking over blocks
// we use 1KB for the UPLOAD_BLOCK_SIZE
ORIGINAL_UPLOAD_BLOCK_SIZE := UPLOAD_BLOCK_SIZE
defer func() {
UPLOAD_BLOCK_SIZE = ORIGINAL_UPLOAD_BLOCK_SIZE
}()
blocks := 10
UPLOAD_BLOCK_SIZE = 10
filename := "fileContent.txt"
file1Content := RandomString(UPLOAD_BLOCK_SIZE*blocks + 5) // intentionally make the data not aligned to a block
file1ContentReader := strings.NewReader(file1Content)
log.Println("Upload fileContent.txt")
uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader, 0)
checkRevisions(protonDrive, ctx, t, filename, 1, 1, 0, 0)
checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename})
{
log.Println("Download fileContent.txt with offset 0")
downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content, 0)
}
{
offset := int64(UPLOAD_BLOCK_SIZE)
log.Println("Download fileContent.txt with offset", offset)
downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset)
}
{
offset := int64(UPLOAD_BLOCK_SIZE + 5)
log.Println("Download fileContent.txt with offset", offset)
downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset)
}
{
offset := int64(UPLOAD_BLOCK_SIZE*blocks/2 + 3)
log.Println("Download fileContent.txt with offset", offset)
downloadFileWithOffset(t, ctx, protonDrive, "", filename, "", file1Content[offset:], offset)
}
log.Println("Delete file fileContent.txt")
deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false, false)
checkActiveFileListing(t, ctx, protonDrive, []string{})
}

View File

@@ -188,6 +188,10 @@ func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *Proton
}
func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string) {
downloadFileWithOffset(t, ctx, protonDrive, parent, name, filepath, data, 0)
}
func downloadFileWithOffset(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string, offset int64) {
parentLink := protonDrive.RootLink
if parent != "" {
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true, false)
@@ -211,7 +215,7 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p
if targetFileLink == nil {
t.Fatalf("File %v not found", name)
} else {
reader, sizeOnServer, fileSystemAttr, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID)
reader, sizeOnServer, fileSystemAttr, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID, offset)
if err != nil {
t.Fatal(err)
}
@@ -228,7 +232,7 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p
if fileSystemAttr.Size != 0 && sizeOnServer == fileSystemAttr.Size {
t.Fatalf("Not possible due to encryption file overhead")
}
if len(downloadedData) != int(fileSystemAttr.Size) {
if offset == 0 && len(downloadedData) != int(fileSystemAttr.Size) {
t.Fatalf("Downloaded file size != uploaded file size: %#v vs %#v", len(downloadedData), int(fileSystemAttr.Size))
}
}
@@ -238,6 +242,7 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p
if err != nil {
t.Fatal(err)
}
originalData = originalData[offset:]
if !bytes.Equal(downloadedData, originalData) {
t.Fatalf("Downloaded content is different from the original content")

View File

@@ -19,4 +19,5 @@ var (
ErrCantFindDraftRevision = errors.New("can't find a draft revision")
ErrWrongUsageOfGetLinkKR = errors.New("internal error for GetLinkKR - nil passed in for link")
ErrWrongUsageOfGetLink = errors.New("internal error for getLink - empty linkID passed in")
ErrSeekOffsetAfterSkippingBlocks = errors.New("internal error for download seek - the offset after skipping blocks is wrong")
)

189
file.go
View File

@@ -4,9 +4,12 @@ import (
"bufio"
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"io"
"log"
"mime"
"os"
"path/filepath"
@@ -20,6 +23,8 @@ import (
type FileSystemAttrs struct {
ModificationTime time.Time
Size int64
BlockSizes []int64
Digests string // sha1 string
}
type FileDownloadReader struct {
@@ -33,12 +38,18 @@ type FileDownloadReader struct {
nextRevision int
isEOF bool
// TODO: integrity check if the entire file is read
}
func (r *FileDownloadReader) Read(p []byte) (int, error) {
if r.data.Len() == 0 {
// TODO: do we have memory sharing bug?
// to avoid sharing the underlying buffer array across re-population
r.data = bytes.NewBuffer(nil)
// we download and decrypt more content
err := r.downloadFileOnRead()
err := r.populateBufferOnRead()
if err != nil {
return 0, err
}
@@ -58,7 +69,33 @@ func (r *FileDownloadReader) Close() error {
return nil
}
func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID string) (io.ReadCloser, int64, *FileSystemAttrs, error) {
func (reader *FileDownloadReader) populateBufferOnRead() error {
if len(reader.revision.Blocks) == 0 || len(reader.revision.Blocks) == reader.nextRevision {
reader.isEOF = true
return nil
}
offset := reader.nextRevision
for i := offset; i-offset < DOWNLOAD_BATCH_BLOCK_SIZE && i < len(reader.revision.Blocks); i++ {
// TODO: parallel download
blockReader, err := reader.protonDrive.c.GetBlock(reader.ctx, reader.revision.Blocks[i].BareURL, reader.revision.Blocks[i].Token)
if err != nil {
return err
}
defer blockReader.Close()
err = decryptBlockIntoBuffer(reader.sessionKey, reader.protonDrive.AddrKR, reader.nodeKR, reader.revision.Blocks[i].Hash, reader.revision.Blocks[i].EncSignature, reader.data, blockReader)
if err != nil {
return err
}
reader.nextRevision = i + 1
}
return nil
}
func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID string, offset int64) (io.ReadCloser, int64, *FileSystemAttrs, error) {
/* It's like event system, we need to get the latest information before creating the move request! */
protonDrive.removeLinkIDFromCache(linkID, false)
@@ -67,7 +104,7 @@ func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID str
return nil, 0, nil, err
}
return protonDrive.DownloadFile(ctx, link)
return protonDrive.DownloadFile(ctx, link, offset)
}
func (protonDrive *ProtonDrive) GetRevisions(ctx context.Context, link *proton.Link, revisionType proton.RevisionState) ([]*proton.RevisionMetadata, error) {
@@ -88,6 +125,15 @@ func (protonDrive *ProtonDrive) GetRevisions(ctx context.Context, link *proton.L
return ret, nil
}
func (protonDrive *ProtonDrive) GetActiveRevisionAttrsByID(ctx context.Context, linkID string) (*FileSystemAttrs, error) {
link, err := protonDrive.getLink(ctx, linkID)
if err != nil {
return nil, err
}
return protonDrive.GetActiveRevisionAttrs(ctx, link)
}
func (protonDrive *ProtonDrive) GetActiveRevisionAttrs(ctx context.Context, link *proton.Link) (*FileSystemAttrs, error) {
if link == nil {
return nil, ErrLinkMustNotBeNil
@@ -120,6 +166,8 @@ func (protonDrive *ProtonDrive) GetActiveRevisionAttrs(ctx context.Context, link
return &FileSystemAttrs{
ModificationTime: modificationTime,
Size: revisionXAttrCommon.Size,
BlockSizes: revisionXAttrCommon.BlockSizes,
Digests: revisionXAttrCommon.Digests,
}, nil
}
@@ -160,10 +208,12 @@ func (protonDrive *ProtonDrive) GetActiveRevisionWithAttrs(ctx context.Context,
return &revision, &FileSystemAttrs{
ModificationTime: modificationTime,
Size: revisionXAttrCommon.Size,
BlockSizes: revisionXAttrCommon.BlockSizes,
Digests: revisionXAttrCommon.Digests,
}, nil
}
func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.Link) (io.ReadCloser, int64, *FileSystemAttrs, error) {
func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.Link, offset int64) (io.ReadCloser, int64, *FileSystemAttrs, error) {
if link.Type != proton.LinkTypeFile {
return nil, 0, nil, ErrLinkTypeMustToBeFileType
}
@@ -201,59 +251,67 @@ func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.L
isEOF: false,
}
err = reader.downloadFileOnRead()
if err != nil {
return nil, 0, nil, err
useFallbackDownload := false
if fileSystemAttrs != nil {
// based on offset, infer the nextRevision (0-based)
if fileSystemAttrs.BlockSizes == nil {
useFallbackDownload = true
} else {
// infer nextRevision
totalBytes := int64(0)
for i := 0; i < len(fileSystemAttrs.BlockSizes); i++ {
prevTotalBytes := totalBytes
totalBytes += fileSystemAttrs.BlockSizes[i]
if offset <= totalBytes {
offset = offset - prevTotalBytes
reader.nextRevision = i
break
}
}
// download will start from the specified block
n, err := io.CopyN(io.Discard, reader, offset)
if err != nil {
return nil, 0, nil, err
}
if int64(n) != offset {
return nil, 0, nil, ErrSeekOffsetAfterSkippingBlocks
}
}
}
if useFallbackDownload {
log.Println("Performing inefficient seek as metadata of encrypted file is missing")
n, err := io.CopyN(io.Discard, reader, offset)
if err != nil {
return nil, 0, nil, err
}
if int64(n) != offset {
return nil, 0, nil, ErrSeekOffsetAfterSkippingBlocks
}
}
return reader, link.Size, fileSystemAttrs, nil
}
func (reader *FileDownloadReader) downloadFileOnRead() error {
if len(reader.revision.Blocks) == 0 || len(reader.revision.Blocks) == reader.nextRevision {
reader.isEOF = true
return nil
}
offset := reader.nextRevision
for i := offset; i-offset < DOWNLOAD_BATCH_BLOCK_SIZE && i < len(reader.revision.Blocks); i++ {
// TODO: parallel download
blockReader, err := reader.protonDrive.c.GetBlock(reader.ctx, reader.revision.Blocks[i].BareURL, reader.revision.Blocks[i].Token)
if err != nil {
return err
}
defer blockReader.Close()
err = decryptBlockIntoBuffer(reader.sessionKey, reader.protonDrive.AddrKR, reader.nodeKR, reader.revision.Blocks[i].Hash, reader.revision.Blocks[i].EncSignature, reader.data, blockReader)
if err != nil {
return err
}
reader.nextRevision = i + 1
}
return nil
}
func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader, testParam int) (string, int64, error) {
func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader, testParam int) (string, *proton.RevisionXAttrCommon, error) {
parentLink, err := protonDrive.getLink(ctx, parentLinkID)
if err != nil {
return "", 0, err
return "", nil, err
}
return protonDrive.uploadFile(ctx, parentLink, filename, modTime, file, testParam)
}
func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string, testParam int) (string, int64, error) {
func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string, testParam int) (string, *proton.RevisionXAttrCommon, error) {
f, err := os.Open(filePath)
if err != nil {
return "", 0, err
return "", nil, err
}
defer f.Close()
info, err := os.Stat(filePath)
if err != nil {
return "", 0, err
return "", nil, err
}
in := bufio.NewReader(f)
@@ -455,14 +513,14 @@ func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, paren
return linkID, revisionID, newSessionKey, newNodeKR, nil
}
func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, newSessionKey *crypto.SessionKey, newNodeKR *crypto.KeyRing, file io.Reader, linkID, revisionID string) ([]byte, int64, error) {
func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, newSessionKey *crypto.SessionKey, newNodeKR *crypto.KeyRing, file io.Reader, linkID, revisionID string) ([]byte, int64, []int64, string, error) {
type PendingUploadBlocks struct {
blockUploadInfo proton.BlockUploadInfo
encData []byte
}
if newSessionKey == nil || newNodeKR == nil {
return nil, 0, ErrMissingInputUploadAndCollectBlockData
return nil, 0, nil, "", ErrMissingInputUploadAndCollectBlockData
}
totalFileSize := int64(0)
@@ -520,11 +578,13 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
}
shouldContinue := true
sha1Digests := sha1.New()
blockSizes := make([]int64, 0)
for i := 1; shouldContinue; i++ {
if (i-1) > 0 && (i-1)%UPLOAD_BATCH_BLOCK_SIZE == 0 {
err := uploadPendingBlocks()
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
}
@@ -541,26 +601,28 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
shouldContinue = false
} else {
// all other errors
return nil, 0, err
return nil, 0, nil, "", err
}
}
data = data[:readBytes]
totalFileSize += int64(readBytes)
sha1Digests.Write(data)
blockSizes = append(blockSizes, int64(readBytes))
// encrypt data
dataPlainMessage := crypto.NewPlainMessage(data)
encData, err := newSessionKey.Encrypt(dataPlainMessage)
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
encSignature, err := protonDrive.AddrKR.SignDetachedEncrypted(dataPlainMessage, newNodeKR)
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
encSignatureStr, err := encSignature.GetArmored()
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
h := sha256.New()
@@ -568,7 +630,7 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
hash := h.Sum(nil)
base64Hash := base64.StdEncoding.EncodeToString(hash)
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
manifestSignatureData = append(manifestSignatureData, hash...)
@@ -584,13 +646,15 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n
}
err := uploadPendingBlocks()
if err != nil {
return nil, 0, err
return nil, 0, nil, "", err
}
return manifestSignatureData, totalFileSize, nil
sha1Hash := sha1Digests.Sum(nil)
sha1String := hex.EncodeToString(sha1Hash)
return manifestSignatureData, totalFileSize, blockSizes, sha1String, nil
}
func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *crypto.KeyRing, modificationTime time.Time, size int64, manifestSignatureData []byte, linkID, revisionID string) error {
func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *crypto.KeyRing, xAttrCommon *proton.RevisionXAttrCommon, manifestSignatureData []byte, linkID, revisionID string) error {
manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData))
if err != nil {
return err
@@ -604,7 +668,8 @@ func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *c
ManifestSignature: manifestSignatureString,
SignatureAddress: protonDrive.signatureAddress,
}
err = commitRevisionReq.SetEncXAttrString(protonDrive.AddrKR, nodeKR, modificationTime, size)
err = commitRevisionReq.SetEncXAttrString(protonDrive.AddrKR, nodeKR, xAttrCommon)
if err != nil {
return err
}
@@ -621,7 +686,7 @@ func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *c
// 0 = normal mode
// 1 = up to create revision
// 2 = up to block upload
func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader, testParam int) (string, int64, error) {
func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader, testParam int) (string, *proton.RevisionXAttrCommon, error) {
// TODO: if we should use github.com/gabriel-vasile/mimetype to detect the MIME type from the file content itself
// Note: this approach might cause the upload progress to display the "fake" progress, since we read in all the content all-at-once
// mimetype.SetLimit(0)
@@ -638,32 +703,38 @@ func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *prot
/* step 1: create a draft */
linkID, revisionID, newSessionKey, newNodeKR, err := protonDrive.createFileUploadDraft(ctx, parentLink, filename, modTime, mimeType)
if err != nil {
return "", 0, err
return "", nil, err
}
if testParam == 1 {
return "", 0, nil
return "", nil, nil
}
/* step 2: upload blocks and collect block data */
manifestSignature, fileSize, err := protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, file, linkID, revisionID)
manifestSignature, fileSize, blockSizes, digests, err := protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, file, linkID, revisionID)
if err != nil {
return "", 0, err
return "", nil, err
}
if testParam == 2 {
// for integration tests
// we try to simulate blocks uploaded but not yet commited
return "", 0, nil
return "", nil, nil
}
/* step 3: mark the file as active by commiting the revision */
err = protonDrive.commitNewRevision(ctx, newNodeKR, modTime, fileSize, manifestSignature, linkID, revisionID)
xAttrCommon := &proton.RevisionXAttrCommon{
ModificationTime: modTime.Format("2006-01-02T15:04:05-0700"), /* ISO8601 */
Size: fileSize,
BlockSizes: blockSizes,
Digests: digests,
}
err = protonDrive.commitNewRevision(ctx, newNodeKR, xAttrCommon, manifestSignature, linkID, revisionID)
if err != nil {
return "", 0, err
return "", nil, err
}
return linkID, fileSize, nil
return linkID, xAttrCommon, nil
}
/*

View File

@@ -106,7 +106,7 @@ func (protonDrive *ProtonDrive) ListDirectoriesRecursively(
log.Println("Downloading", currentPath)
defer log.Println("Completes downloading", currentPath)
reader, _, _, err := protonDrive.DownloadFile(ctx, link)
reader, _, _, err := protonDrive.DownloadFile(ctx, link, 0)
if err != nil {
return err
}

10
go.mod
View File

@@ -3,20 +3,21 @@ module github.com/henrybear327/Proton-API-Bridge
go 1.18
require (
github.com/ProtonMail/gopenpgp/v2 v2.7.1
github.com/henrybear327/go-proton-api v0.0.0-20230713211354-02be61689e29
github.com/ProtonMail/gopenpgp/v2 v2.7.2
github.com/henrybear327/go-proton-api v0.0.0-20230717103708-031d819d74ab
github.com/relvacode/iso8601 v1.3.0
golang.org/x/sync v0.3.0
)
require (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/gluon v0.17.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230710112148-e01326fd72eb // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bradenaw/juniper v0.13.0 // indirect
github.com/bradenaw/juniper v0.13.1 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/emersion/go-message v0.16.0 // indirect
@@ -29,7 +30,6 @@ require (
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
)

16
go.sum
View File

@@ -5,21 +5,21 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9
github.com/ProtonMail/gluon v0.17.0 h1:QfMRUcXd47MANHmoerj1ZHXsNzfW9gjsLmF+7Dim5ZU=
github.com/ProtonMail/gluon v0.17.0/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230710112148-e01326fd72eb h1:RU+Ff2vE68zFQSoBqlb/LChFztEWWJ9EZ8LU4gA3ubU=
github.com/ProtonMail/go-crypto v0.0.0-20230710112148-e01326fd72eb/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
github.com/ProtonMail/gopenpgp/v2 v2.7.2 h1:mIwxSUPezxNYq0RA5106VPWyKC+Ly3FvBUnBJh/7GWw=
github.com/ProtonMail/gopenpgp/v2 v2.7.2/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bradenaw/juniper v0.13.0 h1:KKMAiWDkRt45YUNzzw00Jec4nOgWDLVtztjf39E0ppI=
github.com/bradenaw/juniper v0.13.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bradenaw/juniper v0.13.1 h1:9P7/xeaYuEyqPuJHSHCJoisWyPvZH4FAi59BxJLh7F8=
github.com/bradenaw/juniper v0.13.1/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
@@ -49,8 +49,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/henrybear327/go-proton-api v0.0.0-20230713211354-02be61689e29 h1:OUVzxoIPZ6T4yC5hzvvIEjRaYwom3c9N5VwJgJvr9cs=
github.com/henrybear327/go-proton-api v0.0.0-20230713211354-02be61689e29/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
github.com/henrybear327/go-proton-api v0.0.0-20230717103708-031d819d74ab h1:Lj7+orKyKsOo3UwlopF+IxC7RWdyboAi800RWHiI8Ig=
github.com/henrybear327/go-proton-api v0.0.0-20230717103708-031d819d74ab/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=