Compare commits

..

8 Commits

Author SHA1 Message Date
Caleb Gardner 56fdba2f28 Merge pull request #17 from CalebQ42/fuseBraz
Fuse SUCCESS
2022-12-03 02:50:17 -06:00
Caleb Gardner ffbf4ebc64 Fuse SUCCESS 2022-12-03 02:45:58 -06:00
Belac Darkstorm a015b16293 Clean path before checking if valid. 2022-10-24 03:17:55 -05:00
Caleb Gardner 327781d86e Fixed issues with fragments 2022-08-26 12:11:27 -05:00
Caleb Gardner 4efd2ee49d Merge pull request #16 from tri-adam/0.6.0-fixes
v0.6.0 fixes
2022-08-26 11:44:16 -05:00
Caleb Gardner 392193993c Added single file test 2022-08-26 11:43:46 -05:00
Adam Hughes 2230a449ec fix: use fs interfaces in type assertions
Previous code would panic due to invalid type assertions (presumably due
to change of type returned by func Sub). Switching to relevant fs
interface types fixes the issue and should work going forward, even if
the type is changed.

Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:10:51 +00:00
Adam Hughes 0e50efea64 fix: use correct count when reading fragments
Signed-off-by: Adam Hughes <9903835+tri-adam@users.noreply.github.com>
2022-08-26 15:00:00 +00:00
12 changed files with 426 additions and 67 deletions
+121
View File
@@ -0,0 +1,121 @@
package squashfs
import (
"bytes"
"context"
"io"
"github.com/CalebQ42/fuse"
"github.com/CalebQ42/fuse/fs"
"github.com/CalebQ42/squashfs/internal/inode"
)
func (r *Reader) Mount(mountpoint string) (con *fuse.Conn, err error) {
con, err = fuse.Mount(mountpoint, fuse.ReadOnly())
if err != nil {
return
}
err = fs.Serve(con, &squashFuse{r: r})
return
}
type squashFuse struct {
r *Reader
}
func (s *squashFuse) Root() (fs.Node, error) {
return &fileNode{File: s.r.FS.File}, nil
}
type fileNode struct {
*File
}
func (f *fileNode) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Blocks = f.r.s.Size / 512
if f.r.s.Size%512 > 0 {
attr.Blocks++
}
attr.Gid = f.r.ids[f.i.GidInd]
attr.Inode = uint64(f.i.Num)
attr.Mode = f.i.Mode()
attr.Nlink = f.i.LinkCount()
attr.Size = f.i.Size()
attr.Uid = f.r.ids[f.i.UidInd]
return nil
}
func (f *fileNode) Id() uint64 {
return uint64(f.i.Num)
}
func (f *fileNode) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
return f.SymlinkPath(), nil
}
func (f *fileNode) Lookup(ctx context.Context, name string) (fs.Node, error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
ret, err := asFS.OpenFile(name)
if err != nil {
return nil, fuse.ENOENT
}
return &fileNode{File: ret}, nil
}
func (f *fileNode) ReadAll(ctx context.Context) ([]byte, error) {
if f.IsRegular() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
return buf.Bytes(), err
}
return nil, fuse.ENODATA
}
func (f *fileNode) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
if f.IsRegular() {
buf := make([]byte, req.Size)
n, err := f.File.ReadAt(buf, req.Offset)
if err == io.EOF {
resp.Data = buf[:n]
}
return nil
}
return fuse.ENODATA
}
func (f *fileNode) ReadDirAll(ctx context.Context) (out []fuse.Dirent, err error) {
asFS, err := f.FS()
if err != nil {
return nil, fuse.ENOTDIR
}
var t fuse.DirentType
for i := range asFS.e {
switch asFS.e[i].Type {
case inode.Fil:
t = fuse.DT_File
case inode.Dir:
t = fuse.DT_Dir
case inode.Block:
t = fuse.DT_Block
case inode.Sym:
t = fuse.DT_Link
case inode.Char:
t = fuse.DT_Char
case inode.Fifo:
t = fuse.DT_FIFO
case inode.Sock:
t = fuse.DT_Socket
default:
t = fuse.DT_Unknown
}
out = append(out, fuse.Dirent{
Inode: uint64(asFS.e[i].Num),
Type: t,
Name: asFS.e[i].Name,
})
}
return
}
+5 -2
View File
@@ -3,9 +3,12 @@ module github.com/CalebQ42/squashfs
go 1.19
require (
github.com/klauspost/compress v1.15.9
github.com/pierrec/lz4/v4 v4.1.15
github.com/CalebQ42/fuse v0.1.0
github.com/klauspost/compress v1.15.12
github.com/pierrec/lz4/v4 v4.1.17
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e
github.com/therootcompany/xz v1.0.1
github.com/ulikunitz/xz v0.5.10
)
require golang.org/x/sys v0.2.0 // indirect
+8 -4
View File
@@ -1,10 +1,14 @@
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/CalebQ42/fuse v0.1.0 h1:KLCNjun7zcd2kBNVFfH+SWJyhuwJdE0nhw5/q8K8HGQ=
github.com/CalebQ42/fuse v0.1.0/go.mod h1:pJpoKG03HJKVhsp8o0YQYqmfbFsr3Eowt90yQGQVO+4=
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e h1:dCWirM5F3wMY+cmRda/B1BiPsFtmzXqV9b0hLWtVBMs=
github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+85
View File
@@ -79,6 +79,91 @@ func (r FullReader) process(index int, offset int64, out chan outDat) {
}
}
func (r FullReader) ReadAt(p []byte, off int64) (n int, err error) {
out := make(chan outDat, len(r.sizes))
offset := r.start
num := len(r.sizes)
start := off / int64(r.blockSize)
end := len(p) / int(r.blockSize)
if end%int(r.blockSize) > 0 {
end++
}
if end > len(r.sizes) {
if r.fragRdr != nil {
end = len(r.sizes)
} else {
end = len(r.sizes) + 1
}
}
for i := 0; i < num; i++ {
if i < int(start) || i > end {
offset += uint64(realSize(r.sizes[i]))
continue
}
if i == num-1 && r.fragRdr != nil {
go func() {
rdr, e := r.fragRdr()
if err != nil {
out <- outDat{
i: num - 1,
err: e,
}
return
}
dat, e := io.ReadAll(rdr)
out <- outDat{
i: num - 1,
err: e,
data: dat,
}
if clr, ok := rdr.(io.Closer); ok {
clr.Close()
}
}()
continue
}
go r.process(i, int64(offset), out)
offset += uint64(realSize(r.sizes[i]))
}
cache := make(map[int]outDat)
for cur := start; cur < int64(end); {
dat := <-out
if dat.err != nil {
err = dat.err
return
}
if dat.i != int(cur) {
cache[dat.i] = dat
continue
}
if cur == start {
dat.data = dat.data[off%int64(r.blockSize):]
}
for i := range dat.data {
p[n+i] = dat.data[i]
}
n += len(dat.data)
cur++
var ok bool
for {
dat, ok = cache[int(cur)]
if !ok {
break
}
for i := range dat.data {
p[n+i] = dat.data[i]
}
n += len(dat.data)
cur++
delete(cache, int(cur))
}
}
if n < len(p) {
err = io.EOF
}
return
}
func (r FullReader) WriteTo(w io.Writer) (n int64, err error) {
out := make(chan outDat, len(r.sizes))
offset := r.start
+2
View File
@@ -29,6 +29,7 @@ type Entry struct {
BlockStart uint32
Type uint16
Offset uint16
Num uint32
}
func readEntry(r io.Reader) (e entry, err error) {
@@ -72,6 +73,7 @@ func ReadEntries(rdr io.Reader, size uint32) (e []Entry, err error) {
BlockStart: h.InodeStart,
Type: en.Type,
Offset: en.Offset,
Num: h.Num + uint32(en.NumOffset),
})
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import (
type fileInit struct {
BlockStart uint32
FragInd uint32
Offset uint32
FragOffset uint32
Size uint32
}
@@ -24,7 +24,7 @@ type eFileInit struct {
Sparse uint64
LinkCount uint32
FragInd uint32
Offset uint32
FragOffset uint32
XattrInd uint32
}
+64
View File
@@ -4,6 +4,7 @@ import (
"encoding/binary"
"errors"
"io"
"io/fs"
"strconv"
)
@@ -77,3 +78,66 @@ func Read(r io.Reader, blockSize uint32) (i Inode, err error) {
}
return
}
func (i Inode) Mode() (out fs.FileMode) {
out = fs.FileMode(i.Perm)
switch i.Data.(type) {
case Directory:
out |= fs.ModeDir
case EDirectory:
out |= fs.ModeDir
case Symlink:
out |= fs.ModeSymlink
case ESymlink:
out |= fs.ModeSymlink
case Device:
out |= fs.ModeDevice
case EDevice:
out |= fs.ModeDevice
case IPC:
out |= fs.ModeNamedPipe
case EIPC:
out |= fs.ModeNamedPipe
}
return
}
func (i Inode) LinkCount() uint32 {
switch i.Data.(type) {
case EFile:
return i.Data.(EFile).LinkCount
case Directory:
return i.Data.(Directory).LinkCount
case EDirectory:
return i.Data.(EDirectory).LinkCount
case Device:
return i.Data.(Device).LinkCount
case EDevice:
return i.Data.(EDevice).LinkCount
case IPC:
return i.Data.(IPC).LinkCount
case EIPC:
return i.Data.(EIPC).LinkCount
case Symlink:
return i.Data.(Symlink).LinkCount
case ESymlink:
return i.Data.(ESymlink).LinkCount
default:
return 0
}
}
func (i Inode) Size() uint64 {
switch i.Data.(type) {
case File:
return uint64(i.Data.(File).Size)
case EFile:
return i.Data.(EFile).Size
// case Directory:
// return uint64(i.Data.(Directory).Size)
// case EDirectory:
// return uint64(i.Data.(EDirectory).Size)
default:
return 0
}
}
+4 -4
View File
@@ -96,20 +96,20 @@ func NewReader(r io.ReaderAt) (*Reader, error) {
return nil, err
}
} else {
toRead := squash.s.IdCount
var curRead uint16
toRead := squash.s.FragCount
var curRead uint32
var tmp []fragEntry
var rdr *metadata.Reader
var offset int
for i := range fragOffsets {
curRead = uint16(math.Min(512, float64(toRead)))
curRead = uint32(math.Min(512, float64(toRead)))
tmp = make([]fragEntry, curRead)
rdr = metadata.NewReader(toreader.NewReader(r, int64(fragOffsets[i])), squash.d)
err = binary.Read(rdr, binary.LittleEndian, &tmp)
if err != nil {
return nil, err
}
offset = int(squash.s.IdCount - toRead)
offset = int(squash.s.FragCount - toRead)
for i := range tmp {
squash.fragEntries[offset+i] = tmp[i]
}
+8 -3
View File
@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/CalebQ42/squashfs/internal/data"
"github.com/CalebQ42/squashfs/internal/directory"
"github.com/CalebQ42/squashfs/internal/inode"
)
@@ -18,7 +19,7 @@ import (
type File struct {
i inode.Inode
rdr io.Reader
fullRdr io.WriterTo
fullRdr *data.FullReader
r *Reader
parent *FS
e directory.Entry
@@ -35,7 +36,7 @@ func (r Reader) newFile(en directory.Entry, parent *FS) (*File, error) {
return nil, err
}
var rdr io.Reader
var full io.WriterTo
var full *data.FullReader
if i.Type == inode.Fil || i.Type == inode.EFil {
full, rdr, err = r.getReaders(i)
if err != nil {
@@ -68,6 +69,10 @@ func (f File) Read(p []byte) (int, error) {
return f.rdr.Read(p)
}
func (f File) ReadAt(p []byte, off int64) (int, error) {
return f.fullRdr.ReadAt(p, off)
}
// WriteTo writes all data from the file to the writer. This is multi-threaded.
// The underlying reader is seperate from the one used with Read and can be reused.
func (f File) WriteTo(w io.Writer) (int64, error) {
@@ -220,13 +225,13 @@ func (f File) ExtractWithOptions(folder string, op ExtractionOptions) error {
func (f File) realExtract(folder string, op ExtractionOptions) error {
err := os.MkdirAll(folder, op.FolderPerm)
folder = filepath.Clean(folder)
if err != nil && !os.IsExist(err) {
if op.Verbose {
log.Println("Error while creating extraction folder")
}
return err
}
folder = filepath.Clean(folder)
if f.IsDir() {
filFS, _ := f.FS()
var ents []directory.Entry
+16 -11
View File
@@ -39,8 +39,9 @@ func (r Reader) newFS(e directory.Entry, parent *FS) (*FS, error) {
}, nil
}
//Open opens the file at name. Returns a squashfs.File.
func (f FS) Open(name string) (fs.File, error) {
// Opens the file at name. Returns a squashfs.File.
func (f FS) OpenFile(name string) (*File, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
@@ -48,7 +49,6 @@ func (f FS) Open(name string) (fs.File, error) {
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(name)
if name == "." || name == "" {
return f.File, nil
}
@@ -73,7 +73,7 @@ func (f FS) Open(name string) (fs.File, error) {
Err: err,
}
}
out, err := newFS.Open(strings.Join(split[1:], "/"))
out, err := newFS.OpenFile(strings.Join(split[1:], "/"))
if err != nil {
err.(*fs.PathError).Path = name
}
@@ -96,10 +96,16 @@ func (f FS) Open(name string) (fs.File, error) {
}
}
// Opens the file at name. Returns a io/fs.File.
func (f FS) Open(name string) (fs.File, error) {
return f.OpenFile(name)
}
// Glob returns the name of the files at the given pattern.
// All paths are relative to the FS.
// Uses filepath.Match to compare names.
func (f FS) Glob(pattern string) (out []string, err error) {
pattern = filepath.Clean(pattern)
if !fs.ValidPath(pattern) {
return nil, &fs.PathError{
Op: "glob",
@@ -107,7 +113,6 @@ func (f FS) Glob(pattern string) (out []string, err error) {
Err: fs.ErrInvalid,
}
}
pattern = filepath.Clean(pattern)
split := strings.Split(pattern, "/")
for i := 0; i < len(f.e); i++ {
if match, _ := path.Match(split[0], f.e[i].Name); match {
@@ -131,7 +136,7 @@ func (f FS) Glob(pattern string) (out []string, err error) {
Err: err,
}
}
subGlob, err := sub.(FS).Glob(strings.Join(split[1:], "/"))
subGlob, err := sub.(fs.GlobFS).Glob(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
@@ -159,6 +164,7 @@ func (f FS) Glob(pattern string) (out []string, err error) {
// ReadDir returns all the DirEntry returns all DirEntry's for the directory at name.
// If name is not a directory, returns an error.
func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
name = filepath.Clean(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
@@ -166,7 +172,6 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(name)
if name == "." || name == "" {
return f.File.ReadDir(-1)
}
@@ -208,7 +213,7 @@ func (f FS) ReadDir(name string) ([]fs.DirEntry, error) {
Err: err,
}
}
redDir, err := sub.(FS).ReadDir(strings.Join(split[1:], "/"))
redDir, err := sub.(fs.ReadDirFS).ReadDir(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
@@ -258,6 +263,7 @@ func (f FS) ReadFile(name string) ([]byte, error) {
// Stat returns the fs.FileInfo for the file at name.
func (f FS) Stat(name string) (fs.FileInfo, error) {
name = filepath.Clean(strings.TrimPrefix(name, "/"))
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "stat",
@@ -265,7 +271,6 @@ func (f FS) Stat(name string) (fs.FileInfo, error) {
Err: fs.ErrInvalid,
}
}
name = filepath.Clean(strings.TrimPrefix(name, "/"))
if name == "." || name == "" {
return f.File.Stat()
}
@@ -299,7 +304,7 @@ func (f FS) Stat(name string) (fs.FileInfo, error) {
Err: err,
}
}
stat, err := sub.(FS).Stat(strings.Join(split[1:], "/"))
stat, err := sub.(fs.StatFS).Stat(strings.Join(split[1:], "/"))
if err != nil {
if pathErr, ok := err.(*fs.PathError); ok {
if pathErr.Err == fs.ErrNotExist {
@@ -327,6 +332,7 @@ func (f FS) Stat(name string) (fs.FileInfo, error) {
// Sub returns the FS at dir
func (f FS) Sub(dir string) (fs.FS, error) {
dir = filepath.Clean(dir)
if !fs.ValidPath(dir) {
return nil, &fs.PathError{
Op: "sub",
@@ -334,7 +340,6 @@ func (f FS) Sub(dir string) (fs.FS, error) {
Err: fs.ErrInvalid,
}
}
dir = filepath.Clean(dir)
if dir == "." || dir == "" {
return f, nil
}
+12 -4
View File
@@ -37,13 +37,13 @@ func (r Reader) getReaders(i inode.Inode) (full *data.FullReader, rdr *data.Read
var fragInd uint32
var fragSize uint32
if i.Type == inode.Fil {
fragOffset = uint64(i.Data.(inode.File).Offset)
fragOffset = uint64(i.Data.(inode.File).FragOffset)
blockOffset = uint64(i.Data.(inode.File).BlockStart)
blockSizes = i.Data.(inode.File).BlockSizes
fragInd = i.Data.(inode.File).FragInd
fragSize = i.Data.(inode.File).Size % r.s.BlockSize
} else if i.Type == inode.EFil {
fragOffset = uint64(i.Data.(inode.EFile).Offset)
fragOffset = uint64(i.Data.(inode.EFile).FragOffset)
blockOffset = i.Data.(inode.EFile).BlockStart
blockSizes = i.Data.(inode.EFile).BlockSizes
fragInd = i.Data.(inode.EFile).FragInd
@@ -60,10 +60,14 @@ func (r Reader) getReaders(i inode.Inode) (full *data.FullReader, rdr *data.Read
if err != nil {
return nil, err
}
_, err = fragRdr.Read(make([]byte, fragOffset))
var n, tmpN int
for n != int(fragOffset) {
tmpN, err = fragRdr.Read(make([]byte, int(fragOffset)-n))
if err != nil {
return nil, err
}
n += tmpN
}
fragRdr = io.LimitReader(fragRdr, int64(fragSize))
return fragRdr, nil
})
@@ -72,10 +76,14 @@ func (r Reader) getReaders(i inode.Inode) (full *data.FullReader, rdr *data.Read
if err != nil {
return nil, nil, err
}
_, err = fragRdr.Read(make([]byte, fragOffset))
var n, tmpN int
for n != int(fragOffset) {
tmpN, err = fragRdr.Read(make([]byte, int(fragOffset)-n))
if err != nil {
return nil, nil, err
}
n += tmpN
}
fragRdr = io.LimitReader(fragRdr, int64(fragSize))
rdr.AddFragment(fragRdr)
}
+62
View File
@@ -20,6 +20,8 @@ import (
const (
squashfsURL = "https://darkstorm.tech/LinuxPATest.sfs"
squashfsName = "LinuxPATest.sfs"
filePath = "PortableApps/Notepad++Portable/App/DefaultData/Config/contextMenu.xml"
)
func preTest(dir string) (fil *os.File, err error) {
@@ -55,6 +57,21 @@ func preTest(dir string) (fil *os.File, err error) {
return
}
func TestMisc(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
rdr, err := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
_ = rdr
// Put testing here
t.Fatal("UM")
}
func BenchmarkRace(b *testing.B) {
// tmpDir := b.TempDir()
tmpDir := "testing"
@@ -142,4 +159,49 @@ func TestExtractQuick(t *testing.T) {
if err != nil {
t.Fatal(err)
}
t.Fatal("end")
}
func TestSingleFile(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
os.Remove(filepath.Base(filePath))
rdr, err := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
f, err := rdr.Open(filePath)
if err != nil {
t.Fatal(err)
}
op := squashfs.DefaultOptions()
op.Verbose = true
err = f.(*squashfs.File).ExtractWithOptions("testing", op)
if err != nil {
t.Fatal(err)
}
t.Fatal("HI")
}
func TestFuse(t *testing.T) {
tmpDir := "testing"
fil, err := preTest(tmpDir)
if err != nil {
t.Fatal(err)
}
os.Remove(filepath.Base(filePath))
rdr, err := squashfs.NewReader(fil)
if err != nil {
t.Fatal(err)
}
con, err := rdr.Mount("testing/fuseTest")
if err != nil {
t.Fatal(err)
}
defer con.Close()
<-con.Ready
t.Fatal("testing")
}