Skip to content

Commit

Permalink
fix(examples): use consistent wasi:keyvalue version
Browse files Browse the repository at this point in the history
Signed-off-by: Roman Volosatovs <[email protected]>
  • Loading branch information
rvolosatovs committed Nov 6, 2024
1 parent 2dfd0fa commit b76d8dc
Show file tree
Hide file tree
Showing 31 changed files with 366 additions and 199 deletions.
2 changes: 1 addition & 1 deletion crates/wasi-keyvalue/wit/deps.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[keyvalue]
url = "https://github.com/WebAssembly/wasi-keyvalue/archive/main.tar.gz"
url = "https://github.com/WebAssembly/wasi-keyvalue/archive/da58d54ff969b04e9797dfb986288429cef25e19.tar.gz"
sha256 = "7eaf2e10af6e2de6d5a168893e2656c6685de2009bd37fdc944859f5a6753e55"
sha512 = "ecf0dd2e0b6e5f62c2e10673cc8c2aab86c532f8b19c984fe2ecd9306e9a5c2783183143274477cc4fb3b037b2db2817f248a6bf84b3279180ca4dba832bf5dc"
2 changes: 1 addition & 1 deletion crates/wasi-keyvalue/wit/deps.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
keyvalue = "https://github.com/WebAssembly/wasi-keyvalue/archive/main.tar.gz"
keyvalue = "https://github.com/WebAssembly/wasi-keyvalue/archive/da58d54ff969b04e9797dfb986288429cef25e19.tar.gz"
2 changes: 1 addition & 1 deletion crates/wasi-keyvalue/wit/deps/keyvalue/world.wit
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ world imports {
world watch-service {
include imports;
export watcher;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ type KeyResponse struct {
Keys []string
// The continuation token to use to fetch the next page of keys. If this is `null`, then
// there are no more keys to fetch.
Cursor *uint64
Cursor *string
}

func (v *KeyResponse) String() string { return "KeyResponse" }
Expand Down Expand Up @@ -269,7 +269,7 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er
writes[0] = write0
}
slog.Debug("writing field", "name", "cursor")
write1, err := func(v *uint64, w interface {
write1, err := func(v *string, w interface {
io.ByteWriter
io.Writer
}) (func(wrpc.IndexWriter) error, error) {
Expand All @@ -285,12 +285,26 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er
return nil, fmt.Errorf("failed to write `option::some` status byte: %w", err)
}
slog.Debug("writing `option::some` payload")
write, err := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) {
b := make([]byte, binary.MaxVarintLen64)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing u64")
_, err = w.Write(b[:i])
return err
write, err := (func(wrpc.IndexWriter) error)(nil), func(v string, w io.Writer) (err error) {
n := len(v)
if n > math.MaxUint32 {
return fmt.Errorf("string byte length of %d overflows a 32-bit integer", n)
}
if err = func(v int, w io.Writer) error {
b := make([]byte, binary.MaxVarintLen32)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing string byte length", "len", n)
_, err = w.Write(b[:i])
return err
}(n, w); err != nil {
return fmt.Errorf("failed to write string byte length of %d: %w", n, err)
}
slog.Debug("writing string bytes")
_, err = w.Write([]byte(v))
if err != nil {
return fmt.Errorf("failed to write string bytes: %w", err)
}
return nil
}(*v, w)
if err != nil {
return nil, fmt.Errorf("failed to write `option::some` payload: %w", err)
Expand Down Expand Up @@ -388,18 +402,18 @@ func Open(ctx__ context.Context, wrpc__ wrpc.Invoker, identifier string) (r0__ *
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "open", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "open", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `open`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "open", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "open", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "open", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "open", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[wrpc.Own[Bucket], Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -611,18 +625,18 @@ func Bucket_Get(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Buc
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "bucket.get", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "bucket.get", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `[method]bucket.get`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.get", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.get", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.get", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.get", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[[]uint8, Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -919,18 +933,18 @@ func Bucket_Set(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Buc
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "bucket.set", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "bucket.set", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `[method]bucket.set`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.set", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.set", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.set", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.set", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[struct{}, Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -1104,18 +1118,18 @@ func Bucket_Delete(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "bucket.delete", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "bucket.delete", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `[method]bucket.delete`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.delete", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.delete", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.delete", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.delete", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[struct{}, Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -1290,18 +1304,18 @@ func Bucket_Exists(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "bucket.exists", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "bucket.exists", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `[method]bucket.exists`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.exists", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.exists", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.exists", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.exists", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[bool, Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -1443,7 +1457,7 @@ func Bucket_Exists(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[
// MAY show an out-of-date list of keys if there are concurrent writes to the store.
//
// If any error occurs, it returns an `Err(error)`.
func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Bucket], cursor *uint64) (r0__ *wrpc.Result[KeyResponse, Error], err__ error) {
func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Bucket], cursor *string) (r0__ *wrpc.Result[KeyResponse, Error], err__ error) {
var buf__ bytes.Buffer
write0__, err__ := (func(wrpc.IndexWriter) error)(nil), func(v string, w io.Writer) (err error) {
n := len(v)
Expand All @@ -1470,7 +1484,7 @@ func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borro
err__ = fmt.Errorf("failed to write `self` parameter: %w", err__)
return
}
write1__, err__ := func(v *uint64, w interface {
write1__, err__ := func(v *string, w interface {
io.ByteWriter
io.Writer
}) (func(wrpc.IndexWriter) error, error) {
Expand All @@ -1486,12 +1500,26 @@ func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borro
return nil, fmt.Errorf("failed to write `option::some` status byte: %w", err)
}
slog.Debug("writing `option::some` payload")
write, err := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) {
b := make([]byte, binary.MaxVarintLen64)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing u64")
_, err = w.Write(b[:i])
return err
write, err := (func(wrpc.IndexWriter) error)(nil), func(v string, w io.Writer) (err error) {
n := len(v)
if n > math.MaxUint32 {
return fmt.Errorf("string byte length of %d overflows a 32-bit integer", n)
}
if err = func(v int, w io.Writer) error {
b := make([]byte, binary.MaxVarintLen32)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing string byte length", "len", n)
_, err = w.Write(b[:i])
return err
}(n, w); err != nil {
return fmt.Errorf("failed to write string byte length of %d: %w", n, err)
}
slog.Debug("writing string bytes")
_, err = w.Write([]byte(v))
if err != nil {
return fmt.Errorf("failed to write string bytes: %w", err)
}
return nil
}(*v, w)
if err != nil {
return nil, fmt.Errorf("failed to write `option::some` payload: %w", err)
Expand All @@ -1512,18 +1540,18 @@ func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borro
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft", "bucket.list-keys", buf__.Bytes())
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:keyvalue/[email protected]draft2", "bucket.list-keys", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `[method]bucket.list-keys`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.list-keys", "err", err)
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.list-keys", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft", "name", "[method]bucket.list-keys", "err", cErr__)
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:keyvalue/[email protected]draft2", "name", "[method]bucket.list-keys", "err", cErr__)
}
r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (*wrpc.Result[KeyResponse, Error], error) {
slog.Debug("reading result status byte")
Expand Down Expand Up @@ -1609,7 +1637,7 @@ func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borro
return nil, fmt.Errorf("failed to read `keys` field: %w", err)
}
slog.Debug("reading field", "name", "cursor")
v.Cursor, err = func(r wrpc.IndexReadCloser, path ...uint32) (*uint64, error) {
v.Cursor, err = func(r wrpc.IndexReadCloser, path ...uint32) (*string, error) {
slog.Debug("reading option status byte")
status, err := r.ReadByte()
if err != nil {
Expand All @@ -1620,28 +1648,41 @@ func Bucket_ListKeys(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borro
return nil, nil
case 1:
slog.Debug("reading `option::some` payload")
v, err := func(r io.ByteReader) (uint64, error) {
var x uint64
v, err := func(r interface {
io.ByteReader
io.Reader
}) (string, error) {
var x uint32
var s uint8
for i := 0; i < 10; i++ {
slog.Debug("reading u64 byte", "i", i)
for i := 0; i < 5; i++ {
slog.Debug("reading string length byte", "i", i)
b, err := r.ReadByte()
if err != nil {
if i > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return x, fmt.Errorf("failed to read u64 byte: %w", err)
return "", fmt.Errorf("failed to read string length byte: %w", err)
}
if s == 63 && b > 0x01 {
return x, errors.New("varint overflows a 64-bit integer")
if s == 28 && b > 0x0f {
return "", errors.New("string length overflows a 32-bit integer")
}
if b < 0x80 {
return x | uint64(b)<<s, nil
x = x | uint32(b)<<s
buf := make([]byte, x)
slog.Debug("reading string bytes", "len", x)
_, err = r.Read(buf)
if err != nil {
return "", fmt.Errorf("failed to read string bytes: %w", err)
}
if !utf8.Valid(buf) {
return string(buf), errors.New("string is not valid UTF-8")
}
return string(buf), nil
}
x |= uint64(b&0x7f) << s
x |= uint32(b&0x7f) << s
s += 7
}
return x, errors.New("varint overflows a 64-bit integer")
return "", errors.New("string length overflows a 32-bit integer")
}(r)
if err != nil {
return nil, fmt.Errorf("failed to read `option::some` value: %w", err)
Expand Down
6 changes: 3 additions & 3 deletions examples/go/wasi-keyvalue-client/wit/deps.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[keyvalue]
url = "https://github.com/WebAssembly/wasi-keyvalue/archive/main.tar.gz"
sha256 = "d2de617fe31ec0abc6072f75f97dd22bf95b3231d5b3111471d73871df9081cd"
sha512 = "6f0b4e44c684d760c54552e2bde9bc976e0a4f6525fc1d47acb98625e030847276436242f42a41f4da1bb9169fb2968c53d659d61af9b2f709f4eb6f9880e2c7"
path = "../../../../crates/wasi-keyvalue/wit/deps/keyvalue"
sha256 = "7eaf2e10af6e2de6d5a168893e2656c6685de2009bd37fdc944859f5a6753e55"
sha512 = "ecf0dd2e0b6e5f62c2e10673cc8c2aab86c532f8b19c984fe2ecd9306e9a5c2783183143274477cc4fb3b037b2db2817f248a6bf84b3279180ca4dba832bf5dc"
2 changes: 1 addition & 1 deletion examples/go/wasi-keyvalue-client/wit/deps.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
keyvalue = "https://github.com/WebAssembly/wasi-keyvalue/archive/main.tar.gz"
keyvalue = "../../../../crates/wasi-keyvalue/wit/deps/keyvalue"
26 changes: 25 additions & 1 deletion examples/go/wasi-keyvalue-client/wit/deps/keyvalue/atomic.wit
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,36 @@
interface atomics {
use store.{bucket, error};

/// The error returned by a CAS operation
variant cas-error {
/// A store error occurred when performing the operation
store-error(error),
/// The CAS operation failed because the value was too old. This returns a new CAS handle
/// for easy retries. Implementors MUST return a CAS handle that has been updated to the
/// latest version or transaction.
cas-failed(cas),
}

/// A handle to a CAS (compare-and-swap) operation.
resource cas {
/// Construct a new CAS operation. Implementors can map the underlying functionality
/// (transactions, versions, etc) as desired.
new: static func(bucket: borrow<bucket>, key: string) -> result<cas, error>;
/// Get the current value of the key (if it exists). This allows for avoiding reads if all
/// that is needed to ensure the atomicity of the operation
current: func() -> result<option<list<u8>>, error>;
}

/// Atomically increment the value associated with the key in the store by the given delta. It
/// returns the new value.
///
/// If the key does not exist in the store, it creates a new key-value pair with the value set
/// to the given delta.
///
/// If any other error occurs, it returns an `Err(error)`.
increment: func(bucket: borrow<bucket>, key: string, delta: u64) -> result<u64, error>;
increment: func(bucket: borrow<bucket>, key: string, delta: s64) -> result<s64, error>;

/// Perform the swap on a CAS operation. This consumes the CAS handle and returns an error if
/// the CAS operation failed.
swap: func(cas: cas, value: list<u8>) -> result<_, cas-error>;
}
4 changes: 2 additions & 2 deletions examples/go/wasi-keyvalue-client/wit/deps/keyvalue/store.wit
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface store {
keys: list<string>,
/// The continuation token to use to fetch the next page of keys. If this is `null`, then
/// there are no more keys to fetch.
cursor: option<u64>
cursor: option<string>
}

/// Get the bucket with the specified identifier.
Expand Down Expand Up @@ -117,6 +117,6 @@ interface store {
/// MAY show an out-of-date list of keys if there are concurrent writes to the store.
///
/// If any error occurs, it returns an `Err(error)`.
list-keys: func(cursor: option<u64>) -> result<key-response, error>;
list-keys: func(cursor: option<string>) -> result<key-response, error>;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package wasi:keyvalue@0.2.0-draft;
package wasi:keyvalue@0.2.0-draft2;

/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores.
/// Components targeting this world will be able to do:
Expand Down
2 changes: 1 addition & 1 deletion examples/go/wasi-keyvalue-client/wit/world.wit
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package wrpc-examples:wasi-keyvalue-nats-client;

world client {
import wasi:keyvalue/store@0.2.0-draft;
import wasi:keyvalue/store@0.2.0-draft2;
}
2 changes: 1 addition & 1 deletion examples/go/wasi-keyvalue-server/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (h *Handler) Bucket_Exists(ctx context.Context, bucket wrpc.Borrow[store.Bu
return Ok(ok), nil
}

func (h *Handler) Bucket_ListKeys(ctx context.Context, bucket wrpc.Borrow[store.Bucket], cursor *uint64) (*wrpc.Result[store.KeyResponse, store.Error], error) {
func (h *Handler) Bucket_ListKeys(ctx context.Context, bucket wrpc.Borrow[store.Bucket], cursor *string) (*wrpc.Result[store.KeyResponse, store.Error], error) {
slog.InfoContext(ctx, "handling `wasi:keyvalue/store.bucket.list-keys`", "bucket", bucket, "cursor", cursor)
if cursor != nil {
return wrpc.Err[store.KeyResponse](*store.NewErrorOther("cursors are not supported")), nil
Expand Down
Loading

0 comments on commit b76d8dc

Please sign in to comment.