intial release
This commit is contained in:
parent
abbfcabe8d
commit
dd7e923bff
11 changed files with 1234 additions and 0 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -25,3 +25,8 @@ go.work.sum
|
|||
# env file
|
||||
.env
|
||||
|
||||
# other
|
||||
.vscode/
|
||||
*.AppImage
|
||||
squashfs-root/
|
||||
/lychee-slicer*
|
380
asar/asar.go
Normal file
380
asar/asar.go
Normal file
|
@ -0,0 +1,380 @@
|
|||
package asar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"vaclive.party/software/lychee-slicer/asar/pickle"
|
||||
)
|
||||
|
||||
type EntryMetadata struct {
|
||||
Unpacked *bool `json:"unpacked,omitempty"`
|
||||
}
|
||||
|
||||
type FilesystemDirectoryEntry struct {
|
||||
EntryMetadata
|
||||
Files map[string]FilesystemEntry
|
||||
nezuMeta *map[string]string
|
||||
}
|
||||
|
||||
func (f *FilesystemDirectoryEntry) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
Unpacked *bool `json:"unpacked"`
|
||||
Files *map[string]json.RawMessage `json:"files"`
|
||||
NezuMeta *map[string]string `json:"nezuMeta,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw.Files == nil {
|
||||
return fmt.Errorf("missing files")
|
||||
}
|
||||
f.Unpacked = raw.Unpacked
|
||||
f.Files = make(map[string]FilesystemEntry)
|
||||
f.nezuMeta = raw.NezuMeta
|
||||
for name, entry := range *raw.Files {
|
||||
var dir FilesystemDirectoryEntry
|
||||
if err := json.Unmarshal(entry, &dir); err == nil {
|
||||
f.Files[name] = &dir
|
||||
continue
|
||||
}
|
||||
|
||||
var file FilesystemFileEntry
|
||||
if err := json.Unmarshal(entry, &file); err == nil && file.Size != nil {
|
||||
f.Files[name] = &file
|
||||
continue
|
||||
}
|
||||
|
||||
var link FilesystemLinkEntry
|
||||
if err := json.Unmarshal(entry, &link); err == nil && link.Link != nil {
|
||||
f.Files[name] = &link
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected entry type: %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FilesystemDirectoryEntry) MarshalJSON() ([]byte, error) {
|
||||
raw := struct {
|
||||
Unpacked *bool `json:"unpacked,omitempty"`
|
||||
Files map[string]json.RawMessage `json:"files"`
|
||||
NezuMeta *map[string]string `json:"nezuMeta,omitempty"`
|
||||
}{
|
||||
Unpacked: f.Unpacked,
|
||||
Files: make(map[string]json.RawMessage),
|
||||
NezuMeta: f.nezuMeta,
|
||||
}
|
||||
for name, entry := range f.Files {
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw.Files[name] = data
|
||||
}
|
||||
return json.Marshal(raw)
|
||||
}
|
||||
|
||||
type FilesystemFileEntry struct {
|
||||
EntryMetadata
|
||||
Executable *bool `json:"executable,omitempty"`
|
||||
Offset *string `json:"offset,omitempty"`
|
||||
Size *int64 `json:"size"`
|
||||
Integrity *FileIntegrity `json:"integrity,omitempty"`
|
||||
}
|
||||
|
||||
type FilesystemLinkEntry struct {
|
||||
EntryMetadata
|
||||
Link *string `json:"link"`
|
||||
}
|
||||
|
||||
type FilesystemEntry interface{}
|
||||
|
||||
type Filesystem struct {
|
||||
storage FilesystemStorage
|
||||
header *FilesystemDirectoryEntry
|
||||
}
|
||||
|
||||
func newFileSystemDirectoryEntry() *FilesystemDirectoryEntry {
|
||||
return &FilesystemDirectoryEntry{Files: make(map[string]FilesystemEntry)}
|
||||
}
|
||||
|
||||
func newFileSystemFileEntry() *FilesystemFileEntry {
|
||||
return &FilesystemFileEntry{}
|
||||
}
|
||||
|
||||
func newFileSystemLinkEntry() *FilesystemLinkEntry {
|
||||
return &FilesystemLinkEntry{}
|
||||
}
|
||||
|
||||
func NewFilesystem() *Filesystem {
|
||||
return &Filesystem{
|
||||
storage: *NewFilesystemStorage(bytes.NewReader([]byte{}), NewBytesReadAtWriter([]byte{})),
|
||||
header: newFileSystemDirectoryEntry(),
|
||||
}
|
||||
}
|
||||
|
||||
func OpenFilesystem(reader io.ReaderAt, readerSize int64) (*Filesystem, error) {
|
||||
sizeBytes := make([]byte, 8)
|
||||
if _, err := reader.ReadAt(sizeBytes, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizePickle := pickle.CreateFromBuffer(sizeBytes)
|
||||
headerSize := sizePickle.CreateIterator().ReadUInt32()
|
||||
|
||||
headerBytes := make([]byte, headerSize)
|
||||
if _, err := reader.ReadAt(headerBytes, 8); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headerPickle := pickle.CreateFromBuffer(headerBytes)
|
||||
headerJson := headerPickle.CreateIterator().ReadString()
|
||||
|
||||
var header FilesystemDirectoryEntry
|
||||
if err := json.Unmarshal([]byte(headerJson), &header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sectionReader := io.NewSectionReader(reader, 8+int64(headerSize), readerSize-8-int64(headerSize))
|
||||
|
||||
return &Filesystem{
|
||||
storage: *NewFilesystemStorage(sectionReader, NewBytesReadAtWriter([]byte{})),
|
||||
header: &header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) searchNodeFromDirectory(p string) (*FilesystemDirectoryEntry, error) {
|
||||
entry := fs.header
|
||||
// windows moment
|
||||
dirs := strings.FieldsFunc(p, func(r rune) bool { return r == '/' || r == '\\' })
|
||||
for _, dir := range dirs {
|
||||
if dir != "." {
|
||||
if entry.Files != nil {
|
||||
if _, ok := entry.Files[dir]; !ok {
|
||||
entry.Files[dir] = newFileSystemDirectoryEntry()
|
||||
}
|
||||
entry = entry.Files[dir].(*FilesystemDirectoryEntry)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected directory state while traversing: %s", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) searchNodeFromPath(p string) (FilesystemEntry, error) {
|
||||
if p == "." {
|
||||
return fs.header, nil
|
||||
}
|
||||
println(filepath.Base(p), filepath.Dir(p))
|
||||
name := filepath.Base(p)
|
||||
dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dirNode.Files == nil {
|
||||
dirNode.Files = make(map[string]FilesystemEntry)
|
||||
}
|
||||
if _, ok := dirNode.Files[name]; !ok {
|
||||
dirNode.Files[name] = newFileSystemFileEntry()
|
||||
}
|
||||
return dirNode.Files[name], nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) GetMetaFlag(flag string) string {
|
||||
if fs.header.nezuMeta != nil {
|
||||
if value, ok := (*fs.header.nezuMeta)[flag]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fs *Filesystem) SetMetaFlag(flag string, value string) error {
|
||||
if fs.header.nezuMeta == nil {
|
||||
fs.header.nezuMeta = &map[string]string{}
|
||||
}
|
||||
(*fs.header.nezuMeta)[flag] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) InsertDirectory(p string) (map[string]FilesystemEntry, error) {
|
||||
node, err := fs.searchNodeFromPath(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dirNode := node.(*FilesystemDirectoryEntry)
|
||||
return dirNode.Files, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) InsertFile(p string, data []byte, executable bool) error {
|
||||
size := int64(len(data))
|
||||
if size > math.MaxUint32 {
|
||||
return fmt.Errorf("%s: file size exceeds 4GB", p)
|
||||
}
|
||||
|
||||
dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dirNode.Unpacked != nil && *dirNode.Unpacked {
|
||||
return fmt.Errorf("unpacked directories are not supported: %s", p)
|
||||
}
|
||||
|
||||
node, err := fs.searchNodeFromPath(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileNode := node.(*FilesystemFileEntry)
|
||||
|
||||
fileNode.Size = &size
|
||||
offset := fmt.Sprintf("%d", fs.storage.Size())
|
||||
fileNode.Offset = &offset
|
||||
fileNode.Integrity = getFileIntegrity(data)
|
||||
if executable {
|
||||
fileNode.Executable = &executable
|
||||
}
|
||||
fs.storage.Write(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) ReadFile(p string, followLinks bool) ([]byte, error) {
|
||||
file, err := fs.GetFile(p, followLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if file == nil {
|
||||
return nil, fmt.Errorf("file not found: %s", p)
|
||||
}
|
||||
if linkEntry, ok := file.(*FilesystemLinkEntry); ok {
|
||||
return []byte(*linkEntry.Link), nil
|
||||
}
|
||||
|
||||
fileEntry := file.(*FilesystemFileEntry)
|
||||
if fileEntry.Unpacked != nil && *fileEntry.Unpacked {
|
||||
return nil, fmt.Errorf("unpacked files are not supported: %s", p)
|
||||
}
|
||||
|
||||
offset, err := strconv.ParseInt(*fileEntry.Offset, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]byte, *fileEntry.Size)
|
||||
if _, err := fs.storage.ReadAt(ret, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: integrity check
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) InsertLink(p string, target string) error {
|
||||
dirNode, err := fs.searchNodeFromDirectory(filepath.Dir(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dirNode.Unpacked != nil && *dirNode.Unpacked {
|
||||
return fmt.Errorf("unexpected directories not set to unpack: %s", p)
|
||||
}
|
||||
|
||||
name := filepath.Base(p)
|
||||
if _, ok := dirNode.Files[name]; !ok {
|
||||
dirNode.Files[name] = newFileSystemLinkEntry()
|
||||
}
|
||||
|
||||
linkNode := dirNode.Files[name].(*FilesystemLinkEntry)
|
||||
linkNode.Link = &target
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) ListFiles() []string {
|
||||
files := []string{}
|
||||
|
||||
// recursive function to traverse the filesystem
|
||||
var traverse func(string, FilesystemEntry)
|
||||
traverse = func(p string, entry FilesystemEntry) {
|
||||
switch e := entry.(type) {
|
||||
case *FilesystemDirectoryEntry:
|
||||
for name, child := range e.Files {
|
||||
path := name
|
||||
if p != "" {
|
||||
path = p + "/" + name
|
||||
}
|
||||
traverse(path, child)
|
||||
}
|
||||
case *FilesystemFileEntry:
|
||||
files = append(files, p)
|
||||
case *FilesystemLinkEntry:
|
||||
files = append(files, p)
|
||||
}
|
||||
}
|
||||
|
||||
traverse("", fs.header)
|
||||
return files
|
||||
}
|
||||
|
||||
func (fs *Filesystem) GetNode(p string) (FilesystemEntry, error) {
|
||||
node, err := fs.searchNodeFromDirectory(filepath.Dir(p))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := filepath.Base(p)
|
||||
if name != "" {
|
||||
fmt.Println(p, filepath.Base(p), filepath.Dir(p), name, len(node.Files))
|
||||
return node.Files[name], nil
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) GetFile(p string, followLinks bool) (FilesystemEntry, error) {
|
||||
info, err := fs.GetNode(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if linkEntry, ok := info.(*FilesystemLinkEntry); ok && followLinks {
|
||||
return fs.GetFile(*linkEntry.Link, followLinks)
|
||||
}
|
||||
if _, ok := info.(*FilesystemDirectoryEntry); ok {
|
||||
return nil, fmt.Errorf("not a file: %s", p)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) Save(writer io.Writer) error {
|
||||
headerJson, err := json.Marshal(fs.header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headerPickle := pickle.CreateEmpty()
|
||||
headerPickle.WriteString(string(headerJson))
|
||||
headerBytes := headerPickle.ToBuffer()
|
||||
|
||||
sizePickle := pickle.CreateEmpty()
|
||||
sizePickle.WriteUInt32(uint32(len(headerBytes)))
|
||||
sizeBytes := sizePickle.ToBuffer()
|
||||
|
||||
if _, err := writer.Write(sizeBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := writer.Write(headerBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fs.storage.WriteTo(writer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
42
asar/integrity.go
Normal file
42
asar/integrity.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package asar
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ALGORITHM = "SHA256"
|
||||
BLOCK_SIZE = 4 * 1024 * 1024
|
||||
)
|
||||
|
||||
type FileIntegrity struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Hash string `json:"hash"`
|
||||
BlockSize int `json:"blockSize"`
|
||||
Blocks []string `json:"blocks"`
|
||||
}
|
||||
|
||||
func getFileIntegrity(data []byte) *FileIntegrity {
|
||||
blockHashes := []string{}
|
||||
for i := 0; i < len(data); i += BLOCK_SIZE {
|
||||
end := i + BLOCK_SIZE
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
blockHashes = append(blockHashes, hashBlock(data[i:end]))
|
||||
}
|
||||
|
||||
return &FileIntegrity{
|
||||
Algorithm: ALGORITHM,
|
||||
Hash: hashBlock(data),
|
||||
BlockSize: BLOCK_SIZE,
|
||||
Blocks: blockHashes,
|
||||
}
|
||||
}
|
||||
|
||||
func hashBlock(block []byte) string {
|
||||
hash := crypto.SHA256.New()
|
||||
hash.Write(block)
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
225
asar/pickle/pickle.go
Normal file
225
asar/pickle/pickle.go
Normal file
|
@ -0,0 +1,225 @@
|
|||
package pickle
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
SIZE_UINT32 = 4
|
||||
PAYLOAD_UNIT = 64
|
||||
CAPACITY_READ_ONLY = 1 << 53
|
||||
)
|
||||
|
||||
func alignInt(i, alignment int) int {
|
||||
return i + ((alignment - (i % alignment)) % alignment)
|
||||
}
|
||||
|
||||
type PickleIterator struct {
|
||||
payload []byte
|
||||
payloadOffset int
|
||||
readIndex int
|
||||
endIndex int
|
||||
}
|
||||
|
||||
func NewPickleIterator(pickle *Pickle) *PickleIterator {
|
||||
return &PickleIterator{
|
||||
payload: pickle.header,
|
||||
payloadOffset: pickle.headerSize,
|
||||
readIndex: 0,
|
||||
endIndex: pickle.getPayloadSize(),
|
||||
}
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadBool() bool {
|
||||
return pi.ReadInt() != 0
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadInt() int32 {
|
||||
buf := pi.readBytes(binary.Size(int32(0)))
|
||||
return int32(binary.LittleEndian.Uint32(buf))
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadUInt32() uint32 {
|
||||
buf := pi.readBytes(binary.Size(uint32(0)))
|
||||
return binary.LittleEndian.Uint32(buf)
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadInt64() int64 {
|
||||
buf := pi.readBytes(binary.Size(int64(0)))
|
||||
return int64(binary.LittleEndian.Uint64(buf))
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadUInt64() uint64 {
|
||||
buf := pi.readBytes(binary.Size(uint64(0)))
|
||||
return binary.LittleEndian.Uint64(buf)
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadFloat() float32 {
|
||||
return math.Float32frombits(pi.ReadUInt32())
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) readDouble() float64 {
|
||||
return math.Float64frombits(pi.ReadUInt64())
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) ReadString() string {
|
||||
length := pi.ReadInt()
|
||||
return string(pi.readBytes(int(length)))
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) readBytes(length int) []byte {
|
||||
readPayloadOffset := pi.getReadPayloadOffsetAndAdvance(length)
|
||||
return pi.payload[readPayloadOffset : readPayloadOffset+length]
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) getReadPayloadOffsetAndAdvance(length int) int {
|
||||
if length > pi.endIndex-pi.readIndex {
|
||||
pi.readIndex = pi.endIndex
|
||||
panic(errors.New("failed to read data with length of " + fmt.Sprint(length)))
|
||||
}
|
||||
readPayloadOffset := pi.payloadOffset + pi.readIndex
|
||||
pi.advance(length)
|
||||
return readPayloadOffset
|
||||
}
|
||||
|
||||
func (pi *PickleIterator) advance(size int) {
|
||||
alignedSize := alignInt(size, SIZE_UINT32)
|
||||
if pi.endIndex-pi.readIndex < alignedSize {
|
||||
pi.readIndex = pi.endIndex
|
||||
} else {
|
||||
pi.readIndex += alignedSize
|
||||
}
|
||||
}
|
||||
|
||||
type Pickle struct {
|
||||
header []byte
|
||||
headerSize int
|
||||
capacityAfterHeader int
|
||||
writeOffset int
|
||||
}
|
||||
|
||||
func newPickle(buffer []byte) *Pickle {
|
||||
p := &Pickle{}
|
||||
if buffer != nil {
|
||||
p.header = buffer
|
||||
p.headerSize = len(buffer) - p.getPayloadSize()
|
||||
p.capacityAfterHeader = CAPACITY_READ_ONLY
|
||||
p.writeOffset = 0
|
||||
if p.headerSize > len(buffer) {
|
||||
p.headerSize = 0
|
||||
}
|
||||
if p.headerSize != alignInt(p.headerSize, SIZE_UINT32) {
|
||||
p.headerSize = 0
|
||||
}
|
||||
if p.headerSize == 0 {
|
||||
p.header = make([]byte, 0)
|
||||
}
|
||||
} else {
|
||||
p.header = make([]byte, 0)
|
||||
p.headerSize = SIZE_UINT32
|
||||
p.capacityAfterHeader = 0
|
||||
p.writeOffset = 0
|
||||
p.resize(PAYLOAD_UNIT)
|
||||
p.setPayloadSize(0)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func CreateEmpty() *Pickle {
|
||||
return newPickle(nil)
|
||||
}
|
||||
|
||||
func CreateFromBuffer(buffer []byte) *Pickle {
|
||||
return newPickle(buffer)
|
||||
}
|
||||
|
||||
func (p *Pickle) GetHeader() []byte {
|
||||
return p.header
|
||||
}
|
||||
|
||||
func (p *Pickle) GetHeaderSize() int {
|
||||
return p.headerSize
|
||||
}
|
||||
|
||||
func (p *Pickle) CreateIterator() *PickleIterator {
|
||||
return NewPickleIterator(p)
|
||||
}
|
||||
|
||||
func (p *Pickle) ToBuffer() []byte {
|
||||
return p.header[:p.headerSize+p.getPayloadSize()]
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteBool(value bool) {
|
||||
if value {
|
||||
p.WriteInt(1)
|
||||
}
|
||||
p.WriteInt(0)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteInt(value int32) {
|
||||
p.writeBytes(value, binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteUInt32(value uint32) {
|
||||
p.writeBytes(value, binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteInt64(value int64) {
|
||||
p.writeBytes(value, binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteUInt64(value uint64) {
|
||||
p.writeBytes(value, binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteFloat(value float32) {
|
||||
p.writeBytes(math.Float32bits(value), binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteDouble(value float64) {
|
||||
p.writeBytes(math.Float64bits(value), binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) WriteString(value string) {
|
||||
length := len(value)
|
||||
p.WriteInt(int32(length))
|
||||
p.writeBytes([]byte(value), binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (p *Pickle) setPayloadSize(payloadSize int) {
|
||||
binary.LittleEndian.PutUint32(p.header[:4], uint32(payloadSize))
|
||||
}
|
||||
|
||||
func (p *Pickle) getPayloadSize() int {
|
||||
return int(binary.LittleEndian.Uint32(p.header[:4]))
|
||||
}
|
||||
|
||||
func (p *Pickle) writeBytes(data any, byteOrder binary.ByteOrder) {
|
||||
length := binary.Size(data)
|
||||
if length == -1 {
|
||||
panic(errors.New("unsupported data type"))
|
||||
}
|
||||
dataLength := alignInt(length, SIZE_UINT32)
|
||||
newSize := p.writeOffset + dataLength
|
||||
if newSize > p.capacityAfterHeader {
|
||||
p.resize(max(p.capacityAfterHeader*2, newSize))
|
||||
}
|
||||
binary.Encode(p.header[p.headerSize+p.writeOffset:], byteOrder, data)
|
||||
|
||||
endOffset := p.headerSize + p.writeOffset + length
|
||||
for i := endOffset; i < endOffset+dataLength-length; i++ {
|
||||
p.header[i] = 0
|
||||
}
|
||||
|
||||
p.setPayloadSize(newSize)
|
||||
p.writeOffset = newSize
|
||||
}
|
||||
|
||||
func (p *Pickle) resize(newCapacity int) {
|
||||
newCapacity = alignInt(newCapacity, PAYLOAD_UNIT)
|
||||
p.header = append(p.header, make([]byte, newCapacity)...)
|
||||
p.capacityAfterHeader = newCapacity
|
||||
}
|
99
asar/storage.go
Normal file
99
asar/storage.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package asar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type SizedReaderAt interface {
|
||||
io.ReaderAt
|
||||
Size() int64
|
||||
}
|
||||
|
||||
type WriterReaderAtWriterTo interface {
|
||||
io.Writer
|
||||
io.ReaderAt
|
||||
io.WriterTo
|
||||
}
|
||||
|
||||
type BytesReadAtWriter struct {
|
||||
s []byte
|
||||
i int64
|
||||
}
|
||||
|
||||
func NewBytesReadAtWriter(s []byte) *BytesReadAtWriter {
|
||||
return &BytesReadAtWriter{s: s, i: 0}
|
||||
}
|
||||
|
||||
func (b *BytesReadAtWriter) Write(p []byte) (n int, err error) {
|
||||
b.s = append(b.s, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *BytesReadAtWriter) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
// cannot modify state - see io.ReaderAt
|
||||
if off < 0 {
|
||||
return 0, errors.New("negative offset")
|
||||
}
|
||||
if off >= int64(len(b.s)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n = copy(p, b.s[off:])
|
||||
if n < len(p) {
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *BytesReadAtWriter) WriteTo(w io.Writer) (n int64, err error) {
|
||||
return io.Copy(w, bytes.NewReader(b.s))
|
||||
}
|
||||
|
||||
type FilesystemStorage struct {
|
||||
existing SizedReaderAt
|
||||
new WriterReaderAtWriterTo
|
||||
newOffset int64
|
||||
}
|
||||
|
||||
func NewFilesystemStorage(existing SizedReaderAt, new WriterReaderAtWriterTo) *FilesystemStorage {
|
||||
return &FilesystemStorage{
|
||||
existing: existing,
|
||||
new: new,
|
||||
newOffset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) Size() int64 {
|
||||
return fs.existing.Size() + fs.newOffset
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
existingSize := fs.existing.Size()
|
||||
if off < existingSize {
|
||||
// error when cross boundary
|
||||
if off+int64(len(p)) > existingSize {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
return fs.existing.ReadAt(p, off)
|
||||
}
|
||||
|
||||
return fs.new.ReadAt(p, off-existingSize)
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) Write(p []byte) (n int, err error) {
|
||||
n, err = fs.new.Write(p)
|
||||
fs.newOffset += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *FilesystemStorage) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// write existing
|
||||
n, err = io.Copy(w, io.NewSectionReader(fs.existing, 0, fs.existing.Size()))
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
n2, err := fs.new.WriteTo(w)
|
||||
return n + n2, err
|
||||
}
|
23
go.mod
Normal file
23
go.mod
Normal file
|
@ -0,0 +1,23 @@
|
|||
module vaclive.party/software/lychee-slicer
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1
|
||||
github.com/tdewolff/parse/v2 v2.7.18
|
||||
github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639
|
||||
golang.org/x/sys v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tidwall/gjson v1.14.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
23
go.sum
Normal file
23
go.sum
Normal file
|
@ -0,0 +1,23 @@
|
|||
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1 h1:g7YUigN4dW2+zpdusdTTghZ+5Py3BaUMAStvL8Nk+FY=
|
||||
github.com/elazarl/goproxy v0.0.0-20240909085733-6741dbfc16a1/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo=
|
||||
github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e h1:h+70xO+I2H1tVB5yTRbaPO+JuTTs44o8vWIcX9nhQkw=
|
||||
github.com/ffred/guitocons v0.0.0-20180103100707-e6ef37a75a5e/go.mod h1:EM9wg5H8ZTtFWm2/QsskKRrDc75gCOC6ZBthnYxbbZI=
|
||||
github.com/tdewolff/parse/v2 v2.7.18 h1:uSqjEMT2lwCj5oifBHDcWU2kN1pbLrRENgFWDJa57eI=
|
||||
github.com/tdewolff/parse/v2 v2.7.18/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639 h1:9MJGHk/ErosklMjfeS3xTwvDUET8xHH+b9H2oi/GqbY=
|
||||
github.com/winlabs/gowin32 v0.0.0-20240930213947-f504d7e14639/go.mod h1:N51TYkG9JGR5sytj0EoPl31Xg2kuB507lxEmrwSNvfQ=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
202
main.go
Normal file
202
main.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
|
||||
"github.com/tidwall/sjson"
|
||||
"vaclive.party/software/lychee-slicer/patcher"
|
||||
"vaclive.party/software/lychee-slicer/platform"
|
||||
)
|
||||
|
||||
const ip = "127.0.0.1"
|
||||
|
||||
type graphqlRequest struct {
|
||||
OperationName string `json:"operationName"`
|
||||
}
|
||||
|
||||
type userData struct {
|
||||
operationName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
verbose := flag.Bool("v", false, "should every proxy request be logged to stdout")
|
||||
program := flag.String("program", "", "program to patch and/or proxy")
|
||||
runPatch := flag.Bool("patch", false, "only patch the program")
|
||||
runProxy := flag.Bool("proxy", false, "only start the proxy and program")
|
||||
flag.Parse()
|
||||
if *program == "" {
|
||||
*program = platform.FindProgram()
|
||||
if *program == "" {
|
||||
flag.Usage()
|
||||
log.Fatal("Program not specified")
|
||||
}
|
||||
}
|
||||
|
||||
if !*runPatch && !*runProxy {
|
||||
log.Println("No flags specified, running both patch and proxy")
|
||||
*runProxy = true
|
||||
*runPatch = true
|
||||
}
|
||||
|
||||
if *runPatch {
|
||||
err := patcher.Patch(*program)
|
||||
if err != nil {
|
||||
log.Fatal("Patch:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !*runProxy {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Starting proxy")
|
||||
proxy := goproxy.NewProxyHttpServer()
|
||||
proxy.Verbose = *verbose
|
||||
|
||||
host := "graph.mango3d.io:443"
|
||||
proxy.OnRequest(goproxy.ReqHostIs(host)).HandleConnectFunc(goproxy.AlwaysMitm)
|
||||
proxy.OnRequest(goproxy.ReqHostIs(host)).DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (ret_req *http.Request, ret_res *http.Response) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ctx.Warnf("Panic: %v", e)
|
||||
ret_req = req
|
||||
ret_res = goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadGateway, "Panic")
|
||||
}
|
||||
}()
|
||||
|
||||
if req.Method != "POST" {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Body.Close()
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
var graphql graphqlRequest
|
||||
if err := json.Unmarshal(body, &graphql); err != nil {
|
||||
ctx.Warnf("Unmarshal: %v", err)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
ctx.UserData = &userData{operationName: graphql.OperationName}
|
||||
|
||||
fmt.Println("OperationName:", graphql.OperationName)
|
||||
return req, nil
|
||||
})
|
||||
|
||||
modifyResponse := func(name string, callback func(string) string) {
|
||||
proxy.OnResponse(operationNameIs(name)).DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) (ret *http.Response) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
ret = goproxy.NewResponse(resp.Request, goproxy.ContentTypeText, http.StatusBadGateway, "Panic")
|
||||
}
|
||||
}()
|
||||
|
||||
bb, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
b := string(bb)
|
||||
log.Println(name+":", b)
|
||||
|
||||
b = callback(b)
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewBuffer([]byte(b)))
|
||||
return resp
|
||||
})
|
||||
}
|
||||
|
||||
modifyResponse("GetUser", func(b string) string {
|
||||
b, _ = sjson.Set(b, "data.user.status.isPro", true)
|
||||
b, _ = sjson.Set(b, "data.user.status.isLifetime", true)
|
||||
b, _ = sjson.Set(b, "data.user.status.type", "lifetime")
|
||||
b, _ = sjson.Set(b, "data.user.status.featureTags", []string{"LYCHEE_SLICER_PRO_SLA", "LYCHEE_SLICER_PRO_FDM", "LYCHEE_SLICER_PREMIUM"})
|
||||
b, _ = sjson.Set(b, "data.user.licenses.0.macAddressesLimit", 9999999)
|
||||
return b
|
||||
})
|
||||
|
||||
modifyResponse("userCheck", func(b string) string {
|
||||
b, _ = sjson.Set(b, "data.consumer.user.status.isPremium", true)
|
||||
b, _ = sjson.Set(b, "data.consumer.user.status.isPro", true)
|
||||
b, _ = sjson.Set(b, "data.consumer.user.status.isLifetime", true)
|
||||
b, _ = sjson.Set(b, "data.consumer.user.status.featureTags", []string{"LYCHEE_SLICER_PRO_SLA", "LYCHEE_SLICER_PRO_FDM", "LYCHEE_SLICER_PREMIUM"})
|
||||
b, _ = sjson.Set(b, "data.consumer.user.status.type", "lifetime")
|
||||
return b
|
||||
})
|
||||
|
||||
modifyResponse("checkLicenseWithoutDuration", func(b string) string {
|
||||
b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.isPro", true)
|
||||
b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.isLifetime", true)
|
||||
b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.type", "lifetime")
|
||||
b, _ = sjson.Set(b, "data.checkLicenseWithoutDuration.mac_addresses_limit", 9999999)
|
||||
return b
|
||||
})
|
||||
|
||||
modifyResponse("getLicense", func(b string) string {
|
||||
return b
|
||||
})
|
||||
|
||||
modifyResponse("CanUserGetTrial", func(b string) string {
|
||||
return b
|
||||
})
|
||||
|
||||
port, err := getFreePort()
|
||||
if err != nil {
|
||||
log.Fatal("GetFreePort:", err)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port))
|
||||
if err != nil {
|
||||
log.Fatal("Listen:", err)
|
||||
}
|
||||
|
||||
log.Println("Listening on", ln.Addr().String(), port)
|
||||
|
||||
go func() {
|
||||
cmd := exec.Command(*program, fmt.Sprintf("--proxy-server=%s:%d", ip, port), "--ignore-certificate-errors")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
log.Fatal(cmd.Run())
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
log.Fatal(http.Serve(ln, proxy))
|
||||
}
|
||||
|
||||
func getFreePort() (port int, err error) {
|
||||
var a *net.TCPAddr
|
||||
if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil {
|
||||
var l *net.TCPListener
|
||||
if l, err = net.ListenTCP("tcp", a); err == nil {
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func operationNameIs(operationName string) goproxy.RespCondition {
|
||||
return goproxy.RespConditionFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) bool {
|
||||
if ctx.UserData == nil {
|
||||
return false
|
||||
}
|
||||
userData := ctx.UserData.(*userData)
|
||||
return userData.operationName == operationName
|
||||
})
|
||||
}
|
170
patcher/patcher.go
Normal file
170
patcher/patcher.go
Normal file
|
@ -0,0 +1,170 @@
|
|||
package patcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/tdewolff/parse/v2"
|
||||
"github.com/tdewolff/parse/v2/js"
|
||||
"vaclive.party/software/lychee-slicer/asar"
|
||||
"vaclive.party/software/lychee-slicer/platform"
|
||||
)
|
||||
|
||||
type walker struct {
|
||||
funName string
|
||||
fun *js.FuncDecl
|
||||
}
|
||||
|
||||
func (w *walker) Enter(n js.INode) js.IVisitor {
|
||||
switch n := n.(type) {
|
||||
case *js.BindingElement:
|
||||
if binding, ok := n.Binding.(*js.Var); ok && string(binding.Name()) == w.funName {
|
||||
if fun, ok := n.Default.(*js.FuncDecl); ok {
|
||||
w.fun = fun
|
||||
}
|
||||
}
|
||||
case *js.FuncDecl:
|
||||
if n.Name != nil && string(n.Name.Data) == w.funName {
|
||||
w.fun = n
|
||||
}
|
||||
}
|
||||
|
||||
if w.fun != nil {
|
||||
return nil
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *walker) Exit(n js.INode) {}
|
||||
|
||||
func findFunction(ast *js.AST, name string) *js.FuncDecl {
|
||||
w := &walker{funName: name}
|
||||
js.Walk(w, ast)
|
||||
return w.fun
|
||||
}
|
||||
|
||||
func patchFile(program []byte) ([]byte, error) {
|
||||
ast, err := js.Parse(parse.NewInputBytes([]byte(program)), js.Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fun := findFunction(ast, "verify")
|
||||
if fun == nil {
|
||||
return nil, errors.New("verify function not found")
|
||||
}
|
||||
|
||||
// who needs verification anyway?
|
||||
fun.Body.List = []js.IStmt{
|
||||
&js.ReturnStmt{
|
||||
Value: &js.LiteralExpr{
|
||||
TokenType: js.TrueToken,
|
||||
Data: []byte("true"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stream := bytes.NewBuffer(nil)
|
||||
ast.JS(stream)
|
||||
|
||||
return stream.Bytes(), nil
|
||||
}
|
||||
|
||||
func Patch(program string) error {
|
||||
asarFile := filepath.Join(filepath.Dir(program), "resources", "app.asar")
|
||||
log.Printf("Opening %s", asarFile)
|
||||
|
||||
old, err := os.OpenFile(asarFile, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer old.Close()
|
||||
|
||||
stat, err := old.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs, err := asar.OpenFilesystem(old, stat.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v := fs.GetMetaFlag("patched"); v != "" {
|
||||
log.Printf("Already patched")
|
||||
return nil
|
||||
}
|
||||
|
||||
patch := func(name string) error {
|
||||
log.Printf("Patching %s", name)
|
||||
b, err := fs.ReadFile(name, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err = patchFile(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fs.InsertFile(name, b, false)
|
||||
}
|
||||
|
||||
if err := patch("node_modules/@mango3d/sign/dist/cjs/index.js"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := patch("node_modules/@mango3d/sign/dist/esm/index.js"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fs.SetMetaFlag("patched", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saved, err := os.CreateTemp("", "patched")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer saved.Close()
|
||||
defer os.Remove(saved.Name())
|
||||
|
||||
if err := fs.Save(saved); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs = nil
|
||||
old.Close()
|
||||
|
||||
_, err = saved.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Replacing original asar with patched")
|
||||
new, err := os.Create(asarFile)
|
||||
if err != nil {
|
||||
if err, ok := err.(*os.PathError); ok && platform.IsWindowsRetryable(err.Err) {
|
||||
log.Printf("Windows rename failed, retrying")
|
||||
saved.Close()
|
||||
|
||||
return platform.WindowsRename(saved.Name(), asarFile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer new.Close()
|
||||
|
||||
_, err = io.Copy(new, saved)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
new.Close()
|
||||
saved.Close()
|
||||
|
||||
return err
|
||||
}
|
17
platform/linux.go
Normal file
17
platform/linux.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
//go:build linux
|
||||
|
||||
package platform
|
||||
|
||||
func IsWindowsRetryable(_ error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func WindowsRename(from, to string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func FindProgram() string {
|
||||
// who knows, could be a million things, including this executable itself
|
||||
// let the user/packager decide where to find it
|
||||
return ""
|
||||
}
|
48
platform/windows.go
Normal file
48
platform/windows.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/ffred/guitocons"
|
||||
"github.com/winlabs/gowin32"
|
||||
"github.com/winlabs/gowin32/wrappers"
|
||||
)
|
||||
|
||||
func init() {
|
||||
guitocons.Guitocons()
|
||||
}
|
||||
|
||||
func IsWindowsRetryable(err error) bool {
|
||||
if err == syscall.ERROR_ACCESS_DENIED {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func WindowsRename(from, to string) error {
|
||||
return wrappers.SHFileOperation(&wrappers.SHFILEOPSTRUCT{
|
||||
Func: wrappers.FO_MOVE,
|
||||
From: gowin32.MakeDoubleNullTerminatedLpstr(from),
|
||||
To: gowin32.MakeDoubleNullTerminatedLpstr(to),
|
||||
Flags: wrappers.FOF_NOCONFIRMATION,
|
||||
})
|
||||
}
|
||||
|
||||
func FindProgram() string {
|
||||
// get program files path
|
||||
pf, err := gowin32.GetKnownFolderPath(gowin32.KnownFolderProgramFiles)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
p := filepath.Join(pf, "LycheeSlicer", "LycheeSlicer.exe")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
Loading…
Reference in a new issue