Initial commit
This commit is contained in:
28
ffs/dreamfile.go
Normal file
28
ffs/dreamfile.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"fusetest/furcadia"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: DreamFile ////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type DreamFile struct {
|
||||||
|
Dream *furcadia.PounceDream
|
||||||
|
_attr fuse.Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DreamFile) GetName() string {
|
||||||
|
return f.Dream.ShortURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DreamFile) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
fmt.Printf("[D] DreamFile('%v').Attr()\n", f.GetName())
|
||||||
|
*a = f._attr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
115
ffs/dreamsdir.go
Normal file
115
ffs/dreamsdir.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const (
|
||||||
|
DREAMS_DIR_INODE_BASE = 0x2000
|
||||||
|
DREAMS_DIR_MAX_INODES = 0x1000
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: DreamsDir ////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type DreamsDir struct {
|
||||||
|
_attr fuse.Attr
|
||||||
|
_entries []fuse.Dirent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDreamsDir(inodeBase uint64, rootUID, rootGID uint32) *DreamsDir {
|
||||||
|
now := time.Now()
|
||||||
|
dir := &DreamsDir{
|
||||||
|
_attr: fuse.Attr{
|
||||||
|
Inode: inodeBase,
|
||||||
|
Nlink: 1,
|
||||||
|
Mode: os.ModeDir | 0500,
|
||||||
|
Uid: rootUID,
|
||||||
|
Gid: rootGID,
|
||||||
|
Atime: now,
|
||||||
|
Ctime: now,
|
||||||
|
Mtime: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// add basic entries
|
||||||
|
dir._entries = append(
|
||||||
|
dir._entries,
|
||||||
|
fuse.Dirent{Inode: dir._attr.Inode, Name: ".", Type: fuse.DT_Dir},
|
||||||
|
fuse.Dirent{Inode: ROOT_INODE, Name: "..", Type: fuse.DT_Dir},
|
||||||
|
)
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DreamsDir) GetAttr() *fuse.Attr {
|
||||||
|
return &d._attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DreamsDir) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
*a = d._attr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DreamsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||||
|
pounce := G_FS._pounce
|
||||||
|
dreams := pounce.Dreams
|
||||||
|
items := make([]fuse.Dirent, len(dreams)+2)
|
||||||
|
|
||||||
|
items[0] = fuse.Dirent{Inode: d._attr.Inode, Name: ".", Type: fuse.DT_Dir}
|
||||||
|
items[1] = fuse.Dirent{Inode: ROOT_INODE, Name: "..", Type: fuse.DT_Dir}
|
||||||
|
|
||||||
|
i := 2
|
||||||
|
for shortURL := range dreams {
|
||||||
|
items[i] = fuse.Dirent{
|
||||||
|
Inode: uint64(2000 + i),
|
||||||
|
Name: shortURL,
|
||||||
|
Type: fuse.DT_File,
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DreamsDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||||
|
if name == "." {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dreams := G_FS._pounce.Dreams
|
||||||
|
dream, exists := dreams[name]
|
||||||
|
if !exists {
|
||||||
|
fmt.Printf("[D] DreamsDir().Lookup('%v'): ENOENT\n", name)
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[D] DreamsDir().Lookup('%v'): OK\n", name)
|
||||||
|
now := time.Now()
|
||||||
|
return &DreamFile{
|
||||||
|
Dream: dream,
|
||||||
|
_attr: fuse.Attr{
|
||||||
|
Inode: 0,
|
||||||
|
Nlink: 1,
|
||||||
|
Mode: 0400,
|
||||||
|
Size: 0,
|
||||||
|
Ctime: now,
|
||||||
|
Atime: now,
|
||||||
|
Mtime: now,
|
||||||
|
Uid: d._attr.Uid,
|
||||||
|
Gid: d._attr.Gid,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
67
ffs/fs.go
Normal file
67
ffs/fs.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fusetest/furcadia"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// VAR ////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Filesystem context (there should only be one)
|
||||||
|
G_FS *FurcFS = nil
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: FS ///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type FurcFS struct {
|
||||||
|
RootDir *RootDir
|
||||||
|
_pounce *furcadia.PounceList
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitFS(rootUID, rootGID uint32) *FurcFS {
|
||||||
|
if G_FS != nil {
|
||||||
|
panic("G_FS already initialized - wtf?")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
fs := &FurcFS{
|
||||||
|
RootDir: &RootDir{
|
||||||
|
_nodes: make(map[string]TopLevelDir),
|
||||||
|
_attr: fuse.Attr{
|
||||||
|
Inode: ROOT_INODE,
|
||||||
|
Nlink: 1,
|
||||||
|
Mode: os.ModeDir | 0500,
|
||||||
|
Uid: rootUID,
|
||||||
|
Gid: rootGID,
|
||||||
|
Atime: now,
|
||||||
|
Ctime: now,
|
||||||
|
Mtime: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_pounce: furcadia.NewPounceList(),
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// register root-level items
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
fs.RootDir._nodes["furres"] = NewFurresDir(FURRES_DIR_INODE_BASE, rootUID, rootGID)
|
||||||
|
fs.RootDir._nodes["dreams"] = NewDreamsDir(DREAMS_DIR_INODE_BASE, rootUID, rootGID)
|
||||||
|
|
||||||
|
// FS context is now ready
|
||||||
|
G_FS = fs
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FurcFS) Root() (fs.Node, error) {
|
||||||
|
return fs.RootDir, nil
|
||||||
|
}
|
||||||
28
ffs/furrefile.go
Normal file
28
ffs/furrefile.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"fusetest/furcadia"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: DreamFile ////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type FurreFile struct {
|
||||||
|
Furre *furcadia.PounceFurre
|
||||||
|
_attr fuse.Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FurreFile) GetName() string {
|
||||||
|
return f.Furre.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FurreFile) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
fmt.Printf("[D] FurreFile('%v').Attr()\n", f.GetName())
|
||||||
|
*a = f._attr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
133
ffs/furresdir.go
Normal file
133
ffs/furresdir.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"fusetest/furcadia"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const (
|
||||||
|
FURRES_DIR_INODE_BASE = 0x1000
|
||||||
|
FURRES_DIR_MAX_INODES = 0x1000
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: DreamsDir ////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type FurresDir struct {
|
||||||
|
_attr fuse.Attr
|
||||||
|
_entries []fuse.Dirent
|
||||||
|
_index map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFurresDir(inodeBase uint64, rootUID, rootGID uint32) *FurresDir {
|
||||||
|
now := time.Now()
|
||||||
|
dir := &FurresDir{
|
||||||
|
_attr: fuse.Attr{
|
||||||
|
Inode: inodeBase,
|
||||||
|
Nlink: 1,
|
||||||
|
Mode: os.ModeDir | 0500,
|
||||||
|
Uid: rootUID,
|
||||||
|
Gid: rootGID,
|
||||||
|
Atime: now,
|
||||||
|
Ctime: now,
|
||||||
|
Mtime: now,
|
||||||
|
},
|
||||||
|
_index: make(map[string]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
// add basic entries
|
||||||
|
dir._entries = append(
|
||||||
|
dir._entries,
|
||||||
|
fuse.Dirent{Inode: dir._attr.Inode, Name: ".", Type: fuse.DT_Dir},
|
||||||
|
fuse.Dirent{Inode: ROOT_INODE, Name: "..", Type: fuse.DT_Dir},
|
||||||
|
)
|
||||||
|
|
||||||
|
// add basic entries to the index
|
||||||
|
for i := range dir._entries {
|
||||||
|
dirent := &dir._entries[i]
|
||||||
|
dir._index[dirent.Name] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FurresDir) GetAttr() *fuse.Attr {
|
||||||
|
return &d._attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FurresDir) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
*a = d._attr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FurresDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||||
|
return d._entries, nil
|
||||||
|
// pounce := G_FS._pounce
|
||||||
|
// furres := pounce.Furres
|
||||||
|
// items := make([]fuse.Dirent, len(furres)+2)
|
||||||
|
|
||||||
|
// items[0] = fuse.Dirent{Inode: d._attr.Inode, Name: ".", Type: fuse.DT_Dir}
|
||||||
|
// items[1] = fuse.Dirent{Inode: ROOT_INODE, Name: "..", Type: fuse.DT_Dir}
|
||||||
|
|
||||||
|
// var i uint64 = 2
|
||||||
|
// for _, furre := range furres {
|
||||||
|
// items[i] = fuse.Dirent{
|
||||||
|
// Inode: d._attr.Inode + i - 1,
|
||||||
|
// Name: furre.DisplayName,
|
||||||
|
// Type: fuse.DT_File,
|
||||||
|
// }
|
||||||
|
// i++
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FurresDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||||
|
if name == "." {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// find dirent for the given name
|
||||||
|
|
||||||
|
direntIndex, exists = d._index[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
furres := G_FS._pounce.Furres
|
||||||
|
shortname := furcadia.GetShortName(name)
|
||||||
|
furre, exists := furres[shortname]
|
||||||
|
if !exists {
|
||||||
|
fmt.Printf("[F] FurresDir().Lookup('%v' -> %v): ENOENT\n", name, shortname)
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[F] FurresDir().Lookup('%v'): OK\n", name)
|
||||||
|
now := time.Now()
|
||||||
|
return &FurreFile{
|
||||||
|
Furre: furre,
|
||||||
|
_attr: fuse.Attr{
|
||||||
|
Inode: 0,
|
||||||
|
Nlink: 1,
|
||||||
|
Mode: 0400,
|
||||||
|
Size: 0,
|
||||||
|
Ctime: now,
|
||||||
|
Atime: now,
|
||||||
|
Mtime: now,
|
||||||
|
Uid: d._attr.Uid,
|
||||||
|
Gid: d._attr.Gid,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
61
ffs/rootdir.go
Normal file
61
ffs/rootdir.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const ROOT_INODE = 1
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: RootDir //////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type RootDir struct {
|
||||||
|
_attr fuse.Attr
|
||||||
|
_nodes map[string]TopLevelDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RootDir) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
*a = d._attr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RootDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||||
|
if name == "." {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
node, exists := d._nodes[name]
|
||||||
|
if exists {
|
||||||
|
fmt.Printf("[D] RootDir.Lookup('%v'): OK\n", name)
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[D] RootDir.Lookup('%v'): ENOENT\n", name)
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RootDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||||
|
fmt.Printf("[D] RootDir.ReadDirAll()\n")
|
||||||
|
items := make([]fuse.Dirent, len(d._nodes))
|
||||||
|
i := 0
|
||||||
|
for name, node := range d._nodes {
|
||||||
|
items[i] = fuse.Dirent{
|
||||||
|
Inode: node.GetAttr().Inode,
|
||||||
|
Name: name,
|
||||||
|
Type: fuse.DT_Dir,
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
30
ffs/util.go
Normal file
30
ffs/util.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package ffs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TopLevelDir interface {
|
||||||
|
fs.Node
|
||||||
|
GetAttr() *fuse.Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidName(name string) bool {
|
||||||
|
l := len(name)
|
||||||
|
if l == 0 || l > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ContainsRune(name, '/') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
32
furcadia/character.go
Normal file
32
furcadia/character.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
type Character struct {
|
||||||
|
// e-mail address of the Furcadia account (e.g., artex@furcadia.com)
|
||||||
|
AccountEmail string
|
||||||
|
|
||||||
|
// password for the Furcadia account
|
||||||
|
AccountPassword string
|
||||||
|
|
||||||
|
// display name of the character (with regular spaces) (e.g., "Ice Dragon")
|
||||||
|
DisplayName string
|
||||||
|
|
||||||
|
// shortname of the character (derived from DisplayName) (e.g., "icedragon")
|
||||||
|
ShortName string
|
||||||
|
|
||||||
|
// Furcadia color string
|
||||||
|
Color []byte
|
||||||
|
|
||||||
|
// description text
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCharacter(account, password, displayName string, color []byte, desc string) (*Character, error) {
|
||||||
|
return &Character{
|
||||||
|
AccountEmail: account,
|
||||||
|
AccountPassword: password,
|
||||||
|
DisplayName: displayName,
|
||||||
|
ShortName: GetShortName(displayName),
|
||||||
|
Color: append([]byte(nil), color...),
|
||||||
|
Description: desc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
75
furcadia/client.go
Normal file
75
furcadia/client.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: FurcadiaClient ///////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type FurcadiaClient struct {
|
||||||
|
// Furcadia character to authenticate as
|
||||||
|
Character *Character
|
||||||
|
|
||||||
|
// Network connection object
|
||||||
|
_Conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFurcadiaClient(c Character) *FurcadiaClient {
|
||||||
|
return &FurcadiaClient{
|
||||||
|
Character: &c,
|
||||||
|
_Conn: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected returns true if this furcadia client instance had Connect()
|
||||||
|
// called on it at least once.
|
||||||
|
func (fc *FurcadiaClient) IsConnected() bool {
|
||||||
|
return fc._Conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a connection to the Furcadia game server
|
||||||
|
// (for the first time).
|
||||||
|
//
|
||||||
|
// Note: this method CANNOT be called twice on the same client instance!
|
||||||
|
func (fc *FurcadiaClient) Connect(address string) error {
|
||||||
|
if fc.IsConnected() {
|
||||||
|
return ErrAlreadyConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := net.Dial("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[%v] Cannot connect to %v: %v", fc.String(), address, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[%v] Connected to %v...", fc.String(), address)
|
||||||
|
fc._Conn = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect closes an existing connection to the Furcadia game server.
|
||||||
|
// Calling this method allows future calls to Connect() again. If this instance
|
||||||
|
// was not connected at the time, nothing will happen and nil is returned.
|
||||||
|
//
|
||||||
|
// If closing the underlying network connection produces an error, it will be
|
||||||
|
// returned back to the caller. In either case, the connection will be closed
|
||||||
|
// and no longer owned by this instance.
|
||||||
|
func (fc *FurcadiaClient) Disconnect() error {
|
||||||
|
if !fc.IsConnected() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fc._Conn.Close()
|
||||||
|
fc._Conn = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of a FurcadiaClient instance for
|
||||||
|
// printing/logging purposes
|
||||||
|
func (fc *FurcadiaClient) String() string {
|
||||||
|
return fmt.Sprintf("FurcadiaClient<%s>", fc.Character.DisplayName)
|
||||||
|
}
|
||||||
9
furcadia/consts.go
Normal file
9
furcadia/consts.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEFAULT_GAMESERVER_ADDRESS = "lightbringer.furcadia.com:6500"
|
||||||
|
)
|
||||||
34
furcadia/errors.go
Normal file
34
furcadia/errors.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// ERRORS /////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrAlreadyConnected indicates that a FurcadiaClient instance has already
|
||||||
|
// been connected via a Connect() call, and cannot have it done twice.
|
||||||
|
ErrAlreadyConnected = errors.New("already connected")
|
||||||
|
|
||||||
|
// ErrExists is returned when we are trying to add an entry, but an
|
||||||
|
// equivalent entry is already present and would have otherwise been
|
||||||
|
// overwritten.
|
||||||
|
ErrExists = errors.New("entry exists")
|
||||||
|
|
||||||
|
// ErrInvalidDreamURL is returned when a provided dream url is invalid.
|
||||||
|
ErrInvalidDreamURL = errors.New("invalid dream url")
|
||||||
|
|
||||||
|
// ErrNotFound is returned when we were asked to change an entry, but there
|
||||||
|
// is no entry on record.
|
||||||
|
//
|
||||||
|
// This is not necessarily an error if, say, we are deleting a record which
|
||||||
|
// was already deleted. In such cases, this errors may be ignored.
|
||||||
|
ErrNotFound = errors.New("entry not found")
|
||||||
|
|
||||||
|
// ErrNotImplemented indicates that a requested feature has not yet been
|
||||||
|
// implemented and cannot be used.
|
||||||
|
ErrNotImplemented = errors.New("not implemented")
|
||||||
|
)
|
||||||
292
furcadia/pounce.go
Normal file
292
furcadia/pounce.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const (
|
||||||
|
POUNCE_API_BASE_URL = "http://on.furcadia.com/q/"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: OnlineState //////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type OnlineState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateUnknown = iota
|
||||||
|
StateOffline
|
||||||
|
StateOnline
|
||||||
|
)
|
||||||
|
|
||||||
|
var OnlineStateName = map[OnlineState]string{
|
||||||
|
StateUnknown: "unknown",
|
||||||
|
StateOffline: "offline",
|
||||||
|
StateOnline: "online",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state OnlineState) String() string {
|
||||||
|
return OnlineStateName[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state OnlineState) IsOnline() bool {
|
||||||
|
return state == StateOnline
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: PounceFurre //////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type PounceFurre struct {
|
||||||
|
DisplayName string
|
||||||
|
LastState OnlineState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PounceFurre) GetShortName() string {
|
||||||
|
return GetShortName(pf.DisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: PounceDream //////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type PounceDream struct {
|
||||||
|
Title string // title of the dream, or its full dream url
|
||||||
|
ShortURL string // dream url without the "furc://" prefix or the "/" suffix - just "uploader:name"
|
||||||
|
LastState OnlineState // last known state of this dream
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDreamURL returns a full Furcadia dream URL for this dream.
|
||||||
|
func (pd *PounceDream) GetDreamURL() string {
|
||||||
|
return "furc://" + pd.ShortURL + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: PounceList ///////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type PounceList struct {
|
||||||
|
Furres map[string]*PounceFurre // shortname -> *PounceFurre
|
||||||
|
Dreams map[string]*PounceDream // shortname -> *PounceDream
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPounceList() *PounceList {
|
||||||
|
return &PounceList{
|
||||||
|
Furres: make(map[string]*PounceFurre),
|
||||||
|
Dreams: make(map[string]*PounceDream),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryString returns an HTTP query element of a URL for an online check.
|
||||||
|
// The string contains all furrenly watched furres and dreams whose status our
|
||||||
|
// Pounce client wants to check.
|
||||||
|
func (p *PounceList) GetQueryString() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
for _, furre := range p.Furres {
|
||||||
|
sb.WriteString(fmt.Sprintf("u[]=%s&", furre.GetShortName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dream := range p.Dreams {
|
||||||
|
sb.WriteString(fmt.Sprintf("d[]=%s&", dream.ShortURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSuffix(sb.String(), "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFurre adds a furre to the Pounce list, and returns the record of that
|
||||||
|
// furre back to the caller.
|
||||||
|
//
|
||||||
|
// If a record for the given name already exists, then the existing record is
|
||||||
|
// returned along with ErrExists.
|
||||||
|
func (p *PounceList) AddFurre(name string) (*PounceFurre, error) {
|
||||||
|
shortname := GetShortName(name)
|
||||||
|
oldFurre, ok := p.Furres[shortname]
|
||||||
|
if ok {
|
||||||
|
return oldFurre, ErrExists
|
||||||
|
}
|
||||||
|
|
||||||
|
newFurre := p._newFurre(name)
|
||||||
|
if newFurre.GetShortName() != shortname {
|
||||||
|
panic(fmt.Sprintf("shortname inconsistency: newFurre.GetShortName()=%v; GetShortName(%v)=%v", newFurre.GetShortName(), name, shortname))
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Furres[shortname] = newFurre
|
||||||
|
return newFurre, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFurre deletes a furre from the Pounce list. If no furre by that name
|
||||||
|
// exists, the method returns ErrNotFound.
|
||||||
|
func (p *PounceList) DeleteFurre(name string) error {
|
||||||
|
shortname := GetShortName(name)
|
||||||
|
_, ok := p.Furres[shortname]
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(p.Furres, shortname)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDream adds a dream to the Pounce list, and returns the record of that
|
||||||
|
// dream back to the caller.
|
||||||
|
//
|
||||||
|
// This method accepts the uploader character name, and the "dream name" string
|
||||||
|
// from which a dream url is constructed. The "dreamName" argument MAY be an
|
||||||
|
// empty string.
|
||||||
|
//
|
||||||
|
// If a dream record for the given dream url already exists, then the existing
|
||||||
|
// record is returned instead, along with ErrExists.
|
||||||
|
func (p *PounceList) AddDream(uploaderName, dreamName string) (*PounceDream, error) {
|
||||||
|
shortURL := GetShortDreamURLFromName(uploaderName, dreamName)
|
||||||
|
|
||||||
|
var title string
|
||||||
|
if dreamName == "" {
|
||||||
|
title = uploaderName
|
||||||
|
} else {
|
||||||
|
title = uploaderName + ": " + dreamName
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.__addDream(shortURL, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDreamURL adds a dream to the Pounce list, and returns the record of that
|
||||||
|
// dream back to the caller. This method accepts a Furcadia dream URL.
|
||||||
|
//
|
||||||
|
// If the URL is invalid, ErrInvalidDreamURL will be returned without a
|
||||||
|
// PounceDream object.
|
||||||
|
//
|
||||||
|
// If a dream record for the given dream URL already exists, then the existing
|
||||||
|
// record is returned instead, along with ErrExists.
|
||||||
|
func (p *PounceList) AddDreamURL(url string) (*PounceDream, error) {
|
||||||
|
shortURL, err := GetShortDreamURL(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.__addDream(shortURL, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDream deletes a dream from the Pounce list.
|
||||||
|
// If no dream by this criteria exists, the method returns ErrNotFound.
|
||||||
|
func (p *PounceList) DeleteDream(uploaderName, dreamName string) error {
|
||||||
|
shortURL := GetShortDreamURLFromName(uploaderName, dreamName)
|
||||||
|
return p.__delDream(shortURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDreamURL deletes a dream from the Pouince list.
|
||||||
|
// If no dream by this dream URL exists, the method returns ErrNotFound.
|
||||||
|
// If provided dream URL is invalid, the method returns ErrInvalidDreamURL.
|
||||||
|
func (p *PounceList) DeleteDreamURL(url string) error {
|
||||||
|
shortURL, err := GetShortDreamURL(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.__delDream(shortURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func (p *PounceList) __addDream(shortURL, title string) (*PounceDream, error) {
|
||||||
|
oldDream, ok := p.Dreams[shortURL]
|
||||||
|
if ok {
|
||||||
|
return oldDream, ErrExists
|
||||||
|
}
|
||||||
|
|
||||||
|
dream := p._newDream(shortURL, title)
|
||||||
|
p.Dreams[shortURL] = dream
|
||||||
|
return dream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PounceList) __delDream(shortURL string) error {
|
||||||
|
_, ok := p.Dreams[shortURL]
|
||||||
|
if !ok {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(p.Dreams, shortURL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// _newFurre creates a new PounceFurre object with the given display name.
|
||||||
|
//
|
||||||
|
// displayName is the full name of the furre; it may contain pipes or spaces
|
||||||
|
// in it.
|
||||||
|
func (p *PounceList) _newFurre(displayName string) *PounceFurre {
|
||||||
|
return &PounceFurre{
|
||||||
|
DisplayName: displayName,
|
||||||
|
LastState: StateUnknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// _newDream creates a new PounceDream object with the given parameters.
|
||||||
|
//
|
||||||
|
// The "title" argument specifies a "name" for this dream. If title is an empty
|
||||||
|
// string, then the title will be this dream's full dream url.
|
||||||
|
func (p *PounceList) _newDream(shortDreamURL, title string) *PounceDream {
|
||||||
|
pd := &PounceDream{
|
||||||
|
Title: title,
|
||||||
|
ShortURL: shortDreamURL,
|
||||||
|
LastState: StateUnknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no title provided, use the full dream URL as the title
|
||||||
|
if pd.Title == "" {
|
||||||
|
pd.Title = pd.GetDreamURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pd
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: PounceResponse ///////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const MIN_POLL_INTERVAL_MSEC = 10000
|
||||||
|
|
||||||
|
type PounceResponse struct {
|
||||||
|
NumOnlineFurres int // O422
|
||||||
|
NumOnlineDreams int // D751
|
||||||
|
PollIntervalMsec int // T30000
|
||||||
|
OnlineFurres []string // @10 Albert|Quirky
|
||||||
|
OnlineDreams []string // #1furc://allegria:/ #1furc://allegria/
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePounceResponse(r []byte) (*PounceResponse, error) {
|
||||||
|
//// Example Input:
|
||||||
|
// https://on.furcadia.com/q/?u[]=artex&u[]=albertquirky&d[]=allegria&d[]=ansteorrakingdom
|
||||||
|
//
|
||||||
|
//// Example Output:
|
||||||
|
// T30000 @10 Albert|Quirky O422 #1furc://allegria:/ #1furc://allegria/ #1furc://ansteorrakingdom:medievaltradecity/ #1furc://ansteorrakingdom/ #1furc://ansteorrakingdom:medievaltradecity/ #1furc://ansteorrakingdom/ D751
|
||||||
|
//
|
||||||
|
|
||||||
|
// TODO: Split into lines
|
||||||
|
// TODO: Process each line
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytes creates a response string much like a pounce server would produce.
|
||||||
|
// This method is mostly useful for testing, or if you wish to implement your
|
||||||
|
// own pounce service that's compatible with the official Pounce client.
|
||||||
|
func (r *PounceResponse) ToBytes() (output []byte) {
|
||||||
|
output = fmt.Appendf(output, "T%d\n", r.PollIntervalMsec)
|
||||||
|
for _, furre := range r.OnlineFurres {
|
||||||
|
output = fmt.Appendf(output, "@10 %s\n", furre)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = fmt.Appendf(output, "O%d\n", r.NumOnlineFurres)
|
||||||
|
for _, dreamUrl := range r.OnlineDreams {
|
||||||
|
output = fmt.Appendf(output, "#1%s\n", dreamUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = fmt.Appendf(output, "D%d\n", r.NumOnlineDreams)
|
||||||
|
return
|
||||||
|
}
|
||||||
298
furcadia/pounce_test.go
Normal file
298
furcadia/pounce_test.go
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddFurre(t *testing.T) {
|
||||||
|
testNames := []string{
|
||||||
|
"Artex",
|
||||||
|
"Albert Quirky",
|
||||||
|
"A|B|C",
|
||||||
|
"#SA",
|
||||||
|
}
|
||||||
|
|
||||||
|
pounce := NewPounceList()
|
||||||
|
|
||||||
|
// Success Scenarios //////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
for _, name := range testNames {
|
||||||
|
// check AddFurre() success
|
||||||
|
furre, err := pounce.AddFurre(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("AddFurre(%v) failed: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check display name
|
||||||
|
if furre.DisplayName != name {
|
||||||
|
t.Errorf("AddFurre(%v).DisplayName == %v (mismatch)", name, furre.DisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check shortname
|
||||||
|
expShort := GetShortName(name)
|
||||||
|
furreShort := furre.GetShortName()
|
||||||
|
if furreShort != expShort {
|
||||||
|
t.Errorf("AddFurre(%v).GetShortName() -> %v (expected: %v)", name, furreShort, expShort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pounce.Furres) != len(testNames) {
|
||||||
|
t.Errorf("Length inconsistency: len(pounce.Furres)=%v, len(testNames)=%v", len(pounce.Furres), len(testNames))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad Scenarios //////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// already on record
|
||||||
|
furre, err := pounce.AddFurre("||ARTEX||")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("AddFurre(||ARTEX||) unexpectedly succeeded: %v", furre)
|
||||||
|
}
|
||||||
|
|
||||||
|
// already on record
|
||||||
|
furre, err = pounce.AddFurre("albertquirky")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("AddFurre(albertquirky) unexpectedly succeeded: %v", furre)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteFurre(t *testing.T) {
|
||||||
|
testNames := []string{
|
||||||
|
"Artex",
|
||||||
|
"Albert Quirky",
|
||||||
|
"A|B|C",
|
||||||
|
"#SA",
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a pounce list and populate it with names
|
||||||
|
pounce := NewPounceList()
|
||||||
|
for _, name := range testNames {
|
||||||
|
pounce.AddFurre(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete using a different display name - should work
|
||||||
|
err := pounce.DeleteFurre("ALBERT|QUIRKY")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DeleteFurre(ALBERT|QUIRKY) failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(pounce.Furres) != len(testNames)-1 {
|
||||||
|
t.Errorf("DeleteFurre(ALBERT|QUIRKY) caused invalid furre count: %v (testNames: %v)", len(pounce.Furres), len(testNames))
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete for the second time - expect ErrNotFound
|
||||||
|
err = pounce.DeleteFurre("Albert Quirky")
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("DeleteFurre(Albert Quirky) for a missing record produced: %v (expected ErrNotFound)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete everyone
|
||||||
|
for _, name := range testNames {
|
||||||
|
pounce.DeleteFurre(name)
|
||||||
|
}
|
||||||
|
if len(pounce.Furres) != 0 {
|
||||||
|
t.Errorf("Mass-delete did not empty furre list: %v", pounce.Furres)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddDream(t *testing.T) {
|
||||||
|
pounce := NewPounceList()
|
||||||
|
dream, err := pounce.AddDream("Ice Dragon", "Some dream name")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("AddDream() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test basic output
|
||||||
|
if dream.Title != "Ice Dragon: Some dream name" {
|
||||||
|
t.Errorf("AddDream(): unexpected title '%v'", dream.Title)
|
||||||
|
}
|
||||||
|
if dream.ShortURL != "icedragon:somedreamname" {
|
||||||
|
t.Errorf("AddDream(): unexpected short url '%v'", dream.ShortURL)
|
||||||
|
}
|
||||||
|
if dream.LastState != StateUnknown {
|
||||||
|
t.Errorf("AddDream(): unexpected initial state '%v'", dream.LastState)
|
||||||
|
}
|
||||||
|
if dream.LastState.IsOnline() {
|
||||||
|
t.Errorf("AddDream(): dream is with unknown state is initially online")
|
||||||
|
}
|
||||||
|
if dream.GetDreamURL() != "furc://icedragon:somedreamname/" {
|
||||||
|
t.Errorf("AddDream(): unexpected URL from GetDreamURL(): '%v'", dream.GetDreamURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to add this dream again; make sure it fails
|
||||||
|
d, err := pounce.AddDream("icedragon", "some_dream_name")
|
||||||
|
if !errors.Is(err, ErrExists) {
|
||||||
|
t.Errorf("AddDream(): duplicate add did not produce ErrExists, but %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
if d != dream {
|
||||||
|
t.Errorf("AddDream(): expected existing dream, but got %T(%v)", d, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to add an analogous dream via dreamurl; make sure it also fails
|
||||||
|
d, err = pounce.AddDreamURL(dream.GetDreamURL())
|
||||||
|
if !errors.Is(err, ErrExists) {
|
||||||
|
t.Errorf("AddDreamURL(): analogous duplicate add did not produce ErrExists, but %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
if d != dream {
|
||||||
|
t.Errorf("AddDreamURL(): expected existing dream, but got %T(%v)", d, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddDreamURL(t *testing.T) {
|
||||||
|
const longURL = "furc://a_r_t_e_x:some_dream_name"
|
||||||
|
const normalURL = "furc://artex:somedreamname/"
|
||||||
|
const shortURL = "artex:somedreamname"
|
||||||
|
|
||||||
|
pounce := NewPounceList()
|
||||||
|
dream, err := pounce.AddDreamURL(longURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("AddDreamURL() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test basic output
|
||||||
|
if dream.Title != longURL {
|
||||||
|
t.Errorf("AddDreamURL(): unexpected title '%v' (expected '%v')", dream.Title, longURL)
|
||||||
|
}
|
||||||
|
if dream.ShortURL != shortURL {
|
||||||
|
t.Errorf("AddDreamURL(): unexpected dream short url '%v' (expected '%v')", dream.ShortURL, shortURL)
|
||||||
|
}
|
||||||
|
if dream.LastState != StateUnknown {
|
||||||
|
t.Errorf("AddDream(): unexpected initial state '%v'", dream.LastState)
|
||||||
|
}
|
||||||
|
if dream.LastState.IsOnline() {
|
||||||
|
t.Errorf("AddDream(): dream is with unknown state is initially online")
|
||||||
|
}
|
||||||
|
if dream.GetDreamURL() != normalURL {
|
||||||
|
t.Errorf("AddDream(): unexpected URL from GetDreamURL(): '%v' (expected: '%v')", dream.GetDreamURL(), normalURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to add this dream again; make sure it fails
|
||||||
|
testDup := func(url string) {
|
||||||
|
d, err := pounce.AddDreamURL(url)
|
||||||
|
if !errors.Is(err, ErrExists) {
|
||||||
|
t.Errorf("AddDreamURL(): duplicate add for '%v' did not produce ErrExists, but %T(%v)", url, err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case of ErrExists, d is the existing dream object
|
||||||
|
if d != dream {
|
||||||
|
t.Errorf("AddDreamURL(): expected existing dream for '%v', but got %+v", url, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testDup(longURL)
|
||||||
|
testDup(normalURL)
|
||||||
|
|
||||||
|
// try to add analogous dream via AddDream(); make sure it also fails
|
||||||
|
d, err := pounce.AddDream("Artex", "Some Dream Name")
|
||||||
|
if !errors.Is(err, ErrExists) {
|
||||||
|
t.Errorf("AddDream(): duplicate analogous add did not produce ErrExists, but %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
if d != dream {
|
||||||
|
// in case of ErrExists, d is the existing dream object
|
||||||
|
t.Errorf("AddDream(): expected existing dream, but got %+v", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteDream(t *testing.T) {
|
||||||
|
const uploader = "Ice Dragon"
|
||||||
|
const dreamName = "Some dream name"
|
||||||
|
|
||||||
|
pounce := NewPounceList()
|
||||||
|
|
||||||
|
// try deleting a non-existant dream
|
||||||
|
err := pounce.DeleteDream(uploader, dreamName)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("DeleteDream(): expected ErrNotFound for non-existent dream, but got %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the dream
|
||||||
|
_, err = pounce.AddDream(uploader, dreamName)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("AddDream() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete should succeed now
|
||||||
|
err = pounce.DeleteDream(uploader, dreamName)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DeleteDream() unexpectedly failed with %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dream is no longer available again
|
||||||
|
err = pounce.DeleteDream(uploader, dreamName)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("DeleteDream(): expected ErrNotFound for deleted dream, but got %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteDreamURL(t *testing.T) {
|
||||||
|
const dreamURL = "furc://icedragon:somedreamname"
|
||||||
|
|
||||||
|
pounce := NewPounceList()
|
||||||
|
|
||||||
|
// try deleting a non-existant dream
|
||||||
|
err := pounce.DeleteDreamURL(dreamURL)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("DeleteDreamURL(): expected ErrNotFound for non-existent dream, but got %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the dream
|
||||||
|
_, err = pounce.AddDreamURL(dreamURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("AddDreamURL() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete should succeed now
|
||||||
|
err = pounce.DeleteDreamURL(dreamURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DeleteDreamURL() unexpectedly failed with %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dream is no longer available again
|
||||||
|
err = pounce.DeleteDreamURL(dreamURL)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("DeleteDreamURL(): expected ErrNotFound for deleted dream, but got %T(%v)", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetQueryString(t *testing.T) {
|
||||||
|
pounce := NewPounceList()
|
||||||
|
pounce.AddFurre("Artex")
|
||||||
|
pounce.AddFurre("Albert Quirky")
|
||||||
|
pounce.AddFurre("|Test|Alt|666#SA")
|
||||||
|
pounce.AddDream("Allegria", "")
|
||||||
|
pounce.AddDream("FurN", "")
|
||||||
|
pounce.AddDream("Bramblebrook", "Welcome")
|
||||||
|
pounce.AddDreamURL("furc://artex")
|
||||||
|
pounce.AddDreamURL("furc://artex:somename/")
|
||||||
|
|
||||||
|
query := pounce.GetQueryString() + "&"
|
||||||
|
expectedSubs := []string{
|
||||||
|
"u[]=artex&",
|
||||||
|
"u[]=albertquirky&",
|
||||||
|
"u[]=testalt666sa&",
|
||||||
|
"d[]=allegria&",
|
||||||
|
"d[]=furn&",
|
||||||
|
"d[]=bramblebrook:welcome&",
|
||||||
|
"d[]=artex&",
|
||||||
|
"d[]=artex:somename&",
|
||||||
|
}
|
||||||
|
unexpectedSubs := []string{
|
||||||
|
"&&",
|
||||||
|
}
|
||||||
|
|
||||||
|
nItems := len(strings.Split(query, "&"))
|
||||||
|
if nItems != len(expectedSubs)+1 {
|
||||||
|
t.Errorf("Unexpected number of items (%v) in query: %v", nItems, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range expectedSubs {
|
||||||
|
if !strings.Contains(query, sub) {
|
||||||
|
t.Errorf("Missing substring '%v' in query: %v", sub, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range unexpectedSubs {
|
||||||
|
if strings.Contains(query, sub) {
|
||||||
|
t.Errorf("Unexpected substring '%v' in query: %v", sub, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
furcadia/util.go
Normal file
77
furcadia/util.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// GetShortName returns a "shortname" derived from a given Furcadia display name
|
||||||
|
func GetShortName(displayName string) string {
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
for _, r := range displayName {
|
||||||
|
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||||
|
sb.WriteRune(unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShortDreamURL converts a regular "furc://uploader:name/" dream url into
|
||||||
|
// a "short" form that can be used as a hash key (e.g., "uploader:name").
|
||||||
|
//
|
||||||
|
// ErrInvalidDreamURL is returned if the given url is not a valid dream URL.
|
||||||
|
func GetShortDreamURL(url string) (string, error) {
|
||||||
|
// cut prefix
|
||||||
|
u, found := strings.CutPrefix(strings.ToLower(url), "furc://")
|
||||||
|
if !found {
|
||||||
|
return "", ErrInvalidDreamURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// cut suffix
|
||||||
|
u = strings.SplitN(u, "/", 2)[0]
|
||||||
|
|
||||||
|
// split uploader name and dream name
|
||||||
|
var uploaderName string
|
||||||
|
var dreamName string
|
||||||
|
|
||||||
|
elements := strings.Split(u, ":")
|
||||||
|
switch len(elements) {
|
||||||
|
case 0:
|
||||||
|
panic(fmt.Sprintf("len(%v) == 0", elements))
|
||||||
|
case 1:
|
||||||
|
uploaderName = elements[0]
|
||||||
|
dreamName = ""
|
||||||
|
case 2:
|
||||||
|
uploaderName = elements[0]
|
||||||
|
dreamName = elements[1]
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidDreamURL
|
||||||
|
}
|
||||||
|
|
||||||
|
short := GetShortDreamURLFromName(uploaderName, dreamName)
|
||||||
|
if short == "" || short[0] == ':' {
|
||||||
|
// furc://
|
||||||
|
// furc:///
|
||||||
|
// furc://:/
|
||||||
|
// furc://:something/
|
||||||
|
return "", ErrInvalidDreamURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return short, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetShortDreamURLFromName(uploaderName, dreamName string) (short string) {
|
||||||
|
shortName := GetShortName(uploaderName)
|
||||||
|
shortDream := GetShortName(dreamName)
|
||||||
|
if shortDream == "" {
|
||||||
|
short = shortName
|
||||||
|
} else {
|
||||||
|
short = shortName + ":" + shortDream
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
67
furcadia/util_test.go
Normal file
67
furcadia/util_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package furcadia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetShortName(t *testing.T) {
|
||||||
|
check := func(displayName, expectedShortname string) {
|
||||||
|
short := GetShortName(displayName)
|
||||||
|
if short != expectedShortname {
|
||||||
|
t.Errorf("GetShortName('%v') -> '%v' (expected: '%v')", displayName, short, expectedShortname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check("ArTeX", "artex")
|
||||||
|
check("Ice|Dragon", "icedragon")
|
||||||
|
check("123_arte_X", "123artex")
|
||||||
|
check("|~!@#$%^&*()_+<>[]{}:;'\",./?artex ", "artex")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetShortDreamURL(t *testing.T) {
|
||||||
|
check := func(dreamURL, expectedShortURL string) {
|
||||||
|
short, err := GetShortDreamURL(dreamURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetShortDreamURL('%v') -> unexpected error %T (%v)", dreamURL, err, err)
|
||||||
|
}
|
||||||
|
if short != expectedShortURL {
|
||||||
|
t.Errorf("GetShortDreamURL('%v') -> '%v' (expected: '%v')", dreamURL, short, expectedShortURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check("furc://ice_dragon:some-dream-name/128", "icedragon:somedreamname")
|
||||||
|
check("furc://artex", "artex")
|
||||||
|
check("furc://artex:", "artex")
|
||||||
|
check("furc://artex/128", "artex")
|
||||||
|
check("furc://name:", "name")
|
||||||
|
|
||||||
|
checkErr := func(dreamURL string, expectedErr error) {
|
||||||
|
_, err := GetShortDreamURL(dreamURL)
|
||||||
|
if !errors.Is(err, expectedErr) {
|
||||||
|
t.Errorf("GetShortDreamURL('%v') -> unexpected error %T (%v)", dreamURL, err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkErr("furc://", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc:///", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc://:", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc://:/", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc://:nouploader", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc://:nouploader/", ErrInvalidDreamURL)
|
||||||
|
checkErr("furc://artex::", ErrInvalidDreamURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetShortDreamURLFromName(t *testing.T) {
|
||||||
|
check := func(uploader, name, expected string) {
|
||||||
|
short := GetShortDreamURLFromName(uploader, name)
|
||||||
|
if short != expected {
|
||||||
|
t.Errorf("GetShortDreamURLFromName('%v', '%v') -> '%v' (expected: '%v')", uploader, name, short, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check("Ice Dragon", "", "icedragon")
|
||||||
|
check("Ice Dragon", "Some name here", "icedragon:somenamehere")
|
||||||
|
check("icedragon", "some_name_here", "icedragon:somenamehere")
|
||||||
|
check("", "Illegal but possible", ":illegalbutpossible")
|
||||||
|
}
|
||||||
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module fusetest
|
||||||
|
|
||||||
|
go 1.24.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 // indirect
|
||||||
|
golang.org/x/sys v0.4.0 // indirect
|
||||||
|
)
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 h1:A0NsYy4lDBZAC6QiYeJ4N+XuHIKBpyhAVRMHRQZKTeQ=
|
||||||
|
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5/go.mod h1:gG3RZAMXCa/OTes6rr9EwusmR1OH1tDDy+cg9c5YliY=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
117
main.go
Normal file
117
main.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"fusetest/ffs"
|
||||||
|
"fusetest/furcadia"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bazil.org/fuse"
|
||||||
|
"bazil.org/fuse/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TYPE: File /////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Access(ctx context.Context, req *fuse.AccessRequest) error {
|
||||||
|
fmt.Printf("[D] File[%v].Attr() <- %v\n", f.name, req.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
a.Inode = 12345
|
||||||
|
a.Mode = 0755
|
||||||
|
a.Size = uint64(len(f.data))
|
||||||
|
a.Gid = 999
|
||||||
|
a.Uid = 999
|
||||||
|
a.Atime = time.Now()
|
||||||
|
a.Mtime = time.Now()
|
||||||
|
fmt.Printf("[D] File[%v].Attr() <- %v\n", f.name, a.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) ReadAll(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) ([]byte, error) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
fmt.Printf("[D] File[%v].ReadAll(%v) <- %v\n", f.name, req.String(), resp.String())
|
||||||
|
return append([]byte(nil), f.data...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
end := int(req.Offset) + len(req.Data)
|
||||||
|
if end > len(f.data) {
|
||||||
|
newData := make([]byte, end)
|
||||||
|
copy(newData, f.data)
|
||||||
|
f.data = newData
|
||||||
|
}
|
||||||
|
copy(f.data[req.Offset:], req.Data)
|
||||||
|
resp.Size = len(req.Data)
|
||||||
|
|
||||||
|
fmt.Printf("[D] File[%v].Write(%v) <- %v\n", f.name, req.String(), resp.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
|
||||||
|
fmt.Printf("[D] File[%v].Open(%v) <- %v\n", f.name, req.String(), resp.String())
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
|
||||||
|
fmt.Printf("[D] File[%v].Release(%v)\n", f.name, req.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Forget() {
|
||||||
|
fmt.Printf("[D] File[%v].Forget()\n", f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Printf("Usage: %v <mountpoint>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
mountpoint := os.Args[1]
|
||||||
|
|
||||||
|
c, err := fuse.Mount(
|
||||||
|
mountpoint,
|
||||||
|
fuse.FSName("rawrfs"),
|
||||||
|
fuse.Subtype("rawrfs"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer fuse.Unmount(mountpoint)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
uid := uint32(os.Getuid())
|
||||||
|
gid := uint32(os.Getgid())
|
||||||
|
|
||||||
|
pounce := furcadia.NewPounceList()
|
||||||
|
pounce.AddDream("Artex", "Some dream name")
|
||||||
|
pounce.AddDream("Ice Dragon", "")
|
||||||
|
pounce.AddDream("Albert Quirky", "Welcome!")
|
||||||
|
|
||||||
|
pfs := ffs.InitFS(uid, gid, pounce)
|
||||||
|
if err := fs.Serve(c, pfs); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user