Skip to content
Browse files

gzip/gunzip to/from buffers as well as strings. update init() methods…

… to accept encodings and other option(s), updated docs
  • Loading branch information...
1 parent c6f9096 commit a3dfc78218ae21dc43fa699a5185f5b611d40c0f @Woodya committed Jun 7, 2011
Showing with 338 additions and 186 deletions.
  1. +0 −37 README
  2. +77 −0 README.md
  3. +47 −0 buffer_compat.h
  4. +204 −139 compress.cc
  5. +9 −9 filetest.js
  6. +1 −1 wscript
View
37 README
@@ -1,37 +0,0 @@
-node-compress
-=============
-
-A streaming compression / gzip module for node.js
-
-To install, ensure that you have libz installed, and run:
-
-node-waf configure
-node-waf build
-
-This will put the compress.node binary module in build/default.
-
-
-Quick example
--------------
-
-var compress=require("./compress");
-var sys=require("sys");
-var posix=require("posix");
-
-// Create gzip stream
-var gzip=new compress.Gzip;
-gzip.init();
-
-// Pump data to be compressed
-var gzdata1 = gzip.deflate("My data that needs ", "binary");
-sys.puts("Compressed size : "+gzdata1.length);
-
-var gzdata2 = gzip.deflate("to be compressed. 01234567890.", "binary");
-sys.puts("Compressed size : "+gzdata2.length);
-
-var gzdata3 = gzip.end();
-sys.puts("Last bit : "+gzdata3.length);
-
-// Normally stream this out as its generated, but just print here
-var gzdata = gzdata1+gzdata2+gzdata3;
-sys.puts("Total compressed size : "+gzdata.length);
View
77 README.md
@@ -0,0 +1,77 @@
+NAME
+----
+
+node-compress - A Node.js interface to streaming gzip compression.
+
+INSTALL
+-------
+
+To install, ensure that you have libz installed, and run:
+
+npm install node-compress
+
+or
+
+node-waf configure
+node-waf build
+
+This will put the compress.node binary module in build/default.
+
+Note: on osx I configure may report not finding libz if when build succeeds, still working on this issue.
+
+Quick Gzip example
+------------------
+
+ var compress = require("./compress");
+ var sys = require("sys");
+
+ // Create gzip stream
+ var gzip = new compress.Gzip;
+ // binary string output
+ gzip.init({encoding: 'binary'});
+
+ // Pump data to be compressed
+ var gzdata1 = gzip.deflate("My data that needs ", "ascii");
+ sys.puts("Compressed size : "+gzdata1.length);
+
+ // treat string as binary encoded
+ var gzdata2 = gzip.deflate("to be compressed. 01234567890.");
+ sys.puts("Compressed size : "+gzdata2.length);
+
+ var gzdata3 = gzip.end(); // important to capture end data
+ sys.puts("Last bit : "+gzdata3.length);
+
+ // Normally stream this out as its generated, but just print here
+ var gzdata = gzdata1+gzdata2+gzdata3;
+ sys.puts("Total compressed size : "+gzdata.length);
+
+Quick Gunzip example
+--------------------
+
+ var compress = require("./compress");
+ var sys = require("sys");
+ var fs = require("fs");
+
+ var gunzip = new compress.Gunzip;
+ gunzip.init({encoding: "utf8"});
+
+ var gzdata = fs.readFileSync("somefile.gz", "binary");
+ var inflated = gunzip.inflate(testdata, "binary");
+ gunzip.end(); // returns nothing
+
+Versions
+--------
+
+* 0.1.*:
+ * buffer capable (on by default, this changes the default api use)
+ * buffer input inflate/deflate always allowed, buffer output from same is the default
+ * init accepts options object to configure buffer behavior, compression, and inflated encoding etc:
+ * Gzip.init({encoding: enc, level: [0-9]}), level controls compression level 0 = none, 1 = lowest etc.
+ * Gunzip.init({encoding: enc})
+ * if an encoding is provided to init(), then the output of inflate/deflate will be a string
+ * when providing encodings (either for input our output) for binary data, 'binary' is the only viable encoding, as base64 is not currenlty supported
+ * inflate accepts a buffer or binary string[+encoding[default = 'binary']], output will be a buffer or a string encoded according to init options
+ * deflate accepts a buffer or string[+encoding[default = 'utf8']], output will be a buffer or a string encoded according to init options
+
+* 0.0.*:
+ * all string based, encodings in inflate/deflate methods, no init params
View
47 buffer_compat.h
@@ -0,0 +1,47 @@
+#ifndef BUFFER_COMPAT_H
+#define BUFFER_COMPAT_H
+
+#include <node.h>
+#include <node_buffer.h>
+#include <node_version.h>
+#include <v8.h>
+
+#if NODE_MINOR_VERSION < 3
+
+char *BufferData(node::Buffer *b) {
+ return b->data();
+}
+size_t BufferLength(node::Buffer *b) {
+ return b->length();
+}
+char *BufferData(v8::Local<v8::Object> buf_obj) {
+ v8::HandleScope scope;
+ node::Buffer *buf = node::ObjectWrap::Unwrap<node::Buffer>(buf_obj);
+ return buf->data();
+}
+size_t BufferLength(v8::Local<v8::Object> buf_obj) {
+ v8::HandleScope scope;
+ node::Buffer *buf = node::ObjectWrap::Unwrap<node::Buffer>(buf_obj);
+ return buf->length();
+}
+
+#else // NODE_VERSION
+
+char *BufferData(node::Buffer *b) {
+ return node::Buffer::Data(b->handle_);
+}
+size_t BufferLength(node::Buffer *b) {
+ return node::Buffer::Length(b->handle_);
+}
+char *BufferData(v8::Local<v8::Object> buf_obj) {
+ v8::HandleScope scope;
+ return node::Buffer::Data(buf_obj);
+}
+size_t BufferLength(v8::Local<v8::Object> buf_obj) {
+ v8::HandleScope scope;
+ return node::Buffer::Length(buf_obj);
+}
+
+#endif // NODE_VERSION
+
+#endif//BUFFER_COMPAT_H
View
343 compress.cc
@@ -5,17 +5,29 @@
#include <stdlib.h>
#include <zlib.h>
+#include <stdio.h>
+#include <node_buffer.h>
+#include "buffer_compat.h"
+
#define CHUNK 16384
+#define THROW_IF_NOT(condition, text) if (!(condition)) { \
+ return ThrowException(Exception::Error (String::New(text))); \
+ }
+#define THROW_IF_NOT_A(condition, bufname, x) if (!(condition)) { \
+ char bufname[128] = {0}; \
+ sprintf x; \
+ return ThrowException(Exception::Error (String::New(bufname))); \
+ }
+
using namespace v8;
using namespace node;
+static Persistent<String> use_buffers;
class Gzip : public EventEmitter {
public:
- static void
- Initialize (v8::Handle<v8::Object> target)
- {
+ static void Initialize(v8::Handle<v8::Object> target) {
HandleScope scope;
Local<FunctionTemplate> t = FunctionTemplate::New(New);
@@ -36,42 +48,41 @@ class Gzip : public EventEmitter {
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
+ // +16 to windowBits to write a simple gzip header and trailer around the
+ // compressed data instead of a zlib wrapper
ret = deflateInit2(&strm, level, Z_DEFLATED, 16+MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
return ret;
}
int GzipDeflate(char* data, int data_len, char** out, int* out_len) {
- int ret;
+ int ret = 0;
char* temp;
int i=1;
*out = NULL;
*out_len = 0;
ret = 0;
- if (data_len == 0)
- return 0;
-
- while(data_len>0) {
- if (data_len>CHUNK) {
- strm.avail_in = CHUNK;
+ while (data_len > 0) {
+ if (data_len > CHUNK) {
+ strm.avail_in = CHUNK;
} else {
- strm.avail_in = data_len;
+ strm.avail_in = data_len;
}
strm.next_in = (Bytef*)data;
do {
- temp = (char *)realloc(*out, CHUNK*i +1);
- if (temp == NULL) {
- return Z_MEM_ERROR;
- }
- *out = temp;
- strm.avail_out = CHUNK;
+ temp = (char *)realloc(*out, CHUNK*i +1);
+ if (temp == NULL) {
+ return Z_MEM_ERROR;
+ }
+ *out = temp;
+ strm.avail_out = CHUNK;
strm.next_out = (Bytef*)*out + *out_len;
- ret = deflate(&strm, Z_NO_FLUSH);
- assert(ret != Z_STREAM_ERROR); /* state not clobbered */
- *out_len += (CHUNK - strm.avail_out);
- i++;
+ ret = deflate(&strm, Z_NO_FLUSH);
+ assert(ret != Z_STREAM_ERROR); /* state not clobbered */
+ *out_len += (CHUNK - strm.avail_out);
+ i++;
} while (strm.avail_out == 0);
data += CHUNK;
@@ -80,8 +91,7 @@ class Gzip : public EventEmitter {
return ret;
}
-
- int GzipEnd(char** out, int*out_len) {
+ int GzipEnd(char** out, int* out_len) {
int ret;
char* temp;
int i = 1;
@@ -94,7 +104,7 @@ class Gzip : public EventEmitter {
do {
temp = (char *)realloc(*out, CHUNK*i);
if (temp == NULL) {
- return Z_MEM_ERROR;
+ return Z_MEM_ERROR;
}
*out = temp;
strm.avail_out = CHUNK;
@@ -104,18 +114,14 @@ class Gzip : public EventEmitter {
*out_len += (CHUNK - strm.avail_out);
i++;
} while (strm.avail_out == 0);
-
deflateEnd(&strm);
return ret;
}
-
protected:
- static Handle<Value>
- New (const Arguments& args)
- {
+ static Handle<Value> New(const Arguments& args) {
HandleScope scope;
Gzip *gzip = new Gzip();
@@ -124,96 +130,131 @@ class Gzip : public EventEmitter {
return args.This();
}
- static Handle<Value>
- GzipInit (const Arguments& args)
- {
+ /* options: encoding: string [null] if set output strings, else buffers
+ * level: int [-1] (compression level)
+ */
+ static Handle<Value> GzipInit(const Arguments& args) {
Gzip *gzip = ObjectWrap::Unwrap<Gzip>(args.This());
HandleScope scope;
- int level=Z_DEFAULT_COMPRESSION;
+ int level = Z_DEFAULT_COMPRESSION;
+ gzip->use_buffers = true;
+ if (args.Length() > 0) {
+ THROW_IF_NOT (args[0]->IsObject(), "init argument must be an object");
+ Local<Object> options = args[0]->ToObject();
+ Local<Value> enc = options->Get(String::NewSymbol("encoding"));
+ Local<Value> lev = options->Get(String::NewSymbol("level"));
+
+ if (enc != Undefined() && enc != Null()) {
+ gzip->encoding = ParseEncoding(enc);
+ gzip->use_buffers = false;
+ }
+ if (lev != Undefined() && lev != Null()) {
+ level = lev->ToInt32()->Value();
+ }
+ }
int r = gzip->GzipInit(level);
-
return scope.Close(Integer::New(r));
}
- static Handle<Value>
- GzipDeflate(const Arguments& args) {
+ static Handle<Value> GzipDeflate(const Arguments& args) {
Gzip *gzip = ObjectWrap::Unwrap<Gzip>(args.This());
HandleScope scope;
- enum encoding enc = ParseEncoding(args[1]);
- ssize_t len = DecodeBytes(args[0], enc);
-
- if (len < 0) {
- Local<Value> exception = Exception::TypeError(String::New("Bad argument"));
- return ThrowException(exception);
+ char* buf;
+ ssize_t len;
+ // deflate a buffer or a string?
+ if (Buffer::HasInstance(args[0])) {
+ // buffer
+ Local<Object> buffer = args[0]->ToObject();
+ len = BufferLength(buffer);
+ buf = BufferData(buffer);
+ } else {
+ // string, default encoding is utf8
+ enum encoding enc = args.Length() == 1 ? UTF8 : ParseEncoding(args[1], UTF8);
+ len = DecodeBytes(args[0], enc);
+ THROW_IF_NOT_A (len >= 0, xmsg, (xmsg, "Bad DecodeBytes result: %zd", len));
+
+ // TODO memory leak?
+ buf = new char[len];
+ ssize_t written = DecodeWrite(buf, len, args[0], enc);
+ assert(written == len);
}
- char* buf = new char[len];
- ssize_t written = DecodeWrite(buf, len, args[0], enc);
- assert(written == len);
char* out;
int out_size;
int r = gzip->GzipDeflate(buf, len, &out, &out_size);
-
- if (out_size==0) {
- return scope.Close(String::New(""));
+ THROW_IF_NOT_A (r >= 0, xmsg, (xmsg, "gzip deflate: error(%d) %s", r, gzip->strm.msg));
+ THROW_IF_NOT_A (out_size >= 0, xmsg, (xmsg, "gzip deflate: negative output size: %d", out_size));
+
+ if (gzip->use_buffers) {
+ // output compressed data in a buffer
+ Buffer* b = Buffer::New(out_size);
+ if (out_size != 0) {
+ memcpy(BufferData(b), out, out_size);
+ free(out);
+ }
+ return scope.Close(Local<Value>::New(b->handle_));
+ } else if (out_size == 0) {
+ return scope.Close(String::Empty());
+ } else {
+ // output compressed data in a binary string
+ Local<Value> outString = Encode(out, out_size, gzip->encoding);
+ free(out);
+ return scope.Close(outString);
}
-
- Local<Value> outString = Encode(out, out_size, BINARY);
- free(out);
- return scope.Close(outString);
}
- static Handle<Value>
- GzipEnd(const Arguments& args) {
+ static Handle<Value> GzipEnd(const Arguments& args) {
Gzip *gzip = ObjectWrap::Unwrap<Gzip>(args.This());
HandleScope scope;
char* out;
int out_size;
- bool hex_format = false;
- if (args.Length() > 0 && args[0]->IsString()) {
- String::Utf8Value format_type(args[1]->ToString());
- }
+ int r = gzip->GzipEnd(&out, &out_size);
+ THROW_IF_NOT_A (r >=0, xmsg, (xmsg, "gzip end: error(%d) %s", r, gzip->strm.msg));
+ THROW_IF_NOT_A (out_size >= 0, xmsg, (xmsg, "gzip end: negative output size: %d", out_size));
-
- int r = gzip->GzipEnd( &out, &out_size);
-
- if (out_size==0) {
- return String::New("");
+ if (gzip->use_buffers) {
+ // output compressed data in a buffer
+ Buffer* b = Buffer::New(out_size);
+ if (out_size != 0) {
+ memcpy(BufferData(b), out, out_size);
+ free(out);
+ }
+ return scope.Close(Local<Value>::New(b->handle_));
+ } else if (out_size == 0) {
+ return scope.Close(String::Empty());
+ } else {
+ // output compressed data in a binary string
+ Local<Value> outString = Encode(out, out_size, gzip->encoding);
+ free(out);
+ return scope.Close(outString);
}
- Local<Value> outString = Encode(out, out_size, BINARY);
- free(out);
- return scope.Close(outString);
-
}
-
- Gzip () : EventEmitter ()
- {
+ Gzip() : EventEmitter(), use_buffers(true), encoding(BINARY) {
}
- ~Gzip ()
- {
+ ~Gzip() {
}
private:
z_stream strm;
+ bool use_buffers;
+ enum encoding encoding;
};
class Gunzip : public EventEmitter {
public:
- static void
- Initialize (v8::Handle<v8::Object> target)
- {
+ static void Initialize(v8::Handle<v8::Object> target) {
HandleScope scope;
Local<FunctionTemplate> t = FunctionTemplate::New(New);
@@ -235,67 +276,62 @@ class Gunzip : public EventEmitter {
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;
+ // +16 to decode only the gzip format (no auto-header detection)
int ret = inflateInit2(&strm, 16+MAX_WBITS);
return ret;
}
int GunzipInflate(const char* data, int data_len, char** out, int* out_len) {
- int ret;
+ int ret = 0;
char* temp;
int i=1;
*out = NULL;
*out_len = 0;
- if (data_len == 0)
- return 0;
-
- while(data_len>0) {
- if (data_len>CHUNK) {
- strm.avail_in = CHUNK;
+ while (data_len > 0) {
+ if (data_len > CHUNK) {
+ strm.avail_in = CHUNK;
} else {
- strm.avail_in = data_len;
+ strm.avail_in = data_len;
}
strm.next_in = (Bytef*)data;
do {
- temp = (char *)realloc(*out, CHUNK*i);
- if (temp == NULL) {
- return Z_MEM_ERROR;
- }
- *out = temp;
+ temp = (char *)realloc(*out, CHUNK*i);
+ if (temp == NULL) {
+ return Z_MEM_ERROR;
+ }
+ *out = temp;
strm.avail_out = CHUNK;
- strm.next_out = (Bytef*)*out + *out_len;
- ret = inflate(&strm, Z_NO_FLUSH);
- assert(ret != Z_STREAM_ERROR); /* state not clobbered */
- switch (ret) {
- case Z_NEED_DICT:
- ret = Z_DATA_ERROR; /* and fall through */
- case Z_DATA_ERROR:
- case Z_MEM_ERROR:
- (void)inflateEnd(&strm);
- return ret;
- }
- *out_len += (CHUNK - strm.avail_out);
- i++;
+ strm.next_out = (Bytef*)*out + *out_len;
+ ret = inflate(&strm, Z_NO_FLUSH);
+ assert(ret != Z_STREAM_ERROR); /* state not clobbered */
+ switch (ret) {
+ case Z_NEED_DICT:
+ ret = Z_DATA_ERROR; /* and fall through */
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ (void)inflateEnd(&strm);
+ return ret;
+ }
+ *out_len += (CHUNK - strm.avail_out);
+ i++;
} while (strm.avail_out == 0);
data += CHUNK;
data_len -= CHUNK;
}
return ret;
-
}
-
void GunzipEnd() {
inflateEnd(&strm);
}
protected:
- static Handle<Value>
- New(const Arguments& args) {
+ static Handle<Value> New(const Arguments& args) {
HandleScope scope;
Gunzip *gunzip = new Gunzip();
@@ -304,73 +340,102 @@ class Gunzip : public EventEmitter {
return args.This();
}
- static Handle<Value>
- GunzipInit(const Arguments& args) {
+ /* options: encoding: string [null], if set output strings, else buffers
+ */
+ static Handle<Value> GunzipInit(const Arguments& args) {
Gunzip *gunzip = ObjectWrap::Unwrap<Gunzip>(args.This());
HandleScope scope;
- int r = gunzip->GunzipInit();
+ gunzip->use_buffers = true;
+ if (args.Length() > 0) {
+ THROW_IF_NOT (args[0]->IsObject(), "init argument must be an object");
+ Local<Object> options = args[0]->ToObject();
+ Local<Value> enc = options->Get(String::NewSymbol("encoding"));
+ if (enc != Undefined() && enc != Null()) {
+ gunzip->encoding = ParseEncoding(enc);
+ gunzip->use_buffers = false;
+ }
+ }
+
+ int r = gunzip->GunzipInit();
return scope.Close(Integer::New(r));
}
-
- static Handle<Value>
- GunzipInflate(const Arguments& args) {
+ static Handle<Value> GunzipInflate(const Arguments& args) {
Gunzip *gunzip = ObjectWrap::Unwrap<Gunzip>(args.This());
HandleScope scope;
- enum encoding enc = ParseEncoding(args[1]);
- ssize_t len = DecodeBytes(args[0], BINARY);
-
- if (len < 0) {
- Local<Value> exception = Exception::TypeError(String::New("Bad argument"));
- return ThrowException(exception);
+ char* buf;
+ ssize_t len;
+ // inflate a buffer or a binary string?
+ if (Buffer::HasInstance(args[0])) {
+ // buffer
+ Local<Object> buffer = args[0]->ToObject();
+ len = BufferLength(buffer);
+ buf = BufferData(buffer);
+ } else {
+ // string, default encoding is binary. this is much worse than using a buffer
+ fprintf(stdout, " args.length %d\n", args.Length());
+ enum encoding enc = args.Length() == 1 ? BINARY : ParseEncoding(args[1], BINARY);
+ len = DecodeBytes(args[0], enc);
+ fprintf(stdout, " decodeBytes %zd\n", len);
+ THROW_IF_NOT_A (len >= 0, xmsg, (xmsg, "Bad DecodeBytes result: %zd", len));
+
+ // TODO memory leak?
+ buf = new char[len];
+ ssize_t written = DecodeWrite(buf, len, args[0], enc);
+ assert(written == len);
}
- char* buf = new char[len];
- ssize_t written = DecodeWrite(buf, len, args[0], BINARY);
- assert(written == len);
-
char* out;
int out_size;
int r = gunzip->GunzipInflate(buf, len, &out, &out_size);
-
- Local<Value> outString = Encode(out, out_size, enc);
- free(out);
- return scope.Close(outString);
+ THROW_IF_NOT_A (r >= 0, xmsg, (xmsg, "gunzip inflate: error(%d) %s", r, gunzip->strm.msg));
+ THROW_IF_NOT_A (out_size >= 0, xmsg, (xmsg, "gunzip inflate: negative output size: %d", out_size));
+
+ if (gunzip->use_buffers) {
+ // output decompressed data in a buffer
+ Buffer* b = Buffer::New(out_size);
+ if (out_size != 0) {
+ memcpy(BufferData(b), out, out_size);
+ free(out);
+ }
+ return scope.Close(Local<Value>::New(b->handle_));
+ } else if (out_size == 0) {
+ return scope.Close(String::Empty());
+ } else {
+ // output decompressed data in an encoded string
+ Local<Value> outString = Encode(out, out_size, gunzip->encoding);
+ free(out);
+ return scope.Close(outString);
+ }
}
- static Handle<Value>
- GunzipEnd(const Arguments& args) {
+ static Handle<Value> GunzipEnd(const Arguments& args) {
Gunzip *gunzip = ObjectWrap::Unwrap<Gunzip>(args.This());
HandleScope scope;
-
gunzip->GunzipEnd();
-
- return scope.Close(String::New(""));
+ return scope.Close(Undefined());
}
- Gunzip () : EventEmitter ()
- {
+ Gunzip() : EventEmitter(), use_buffers(true), encoding(BINARY) {
}
- ~Gunzip ()
- {
+ ~Gunzip() {
}
private:
- z_stream strm;
-
+ z_stream strm;
+ bool use_buffers;
+ enum encoding encoding;
};
-extern "C" void
-init (Handle<Object> target)
-{
+extern "C" void init(Handle<Object> target) {
HandleScope scope;
Gzip::Initialize(target);
Gunzip::Initialize(target);
View
18 filetest.js
@@ -4,7 +4,7 @@ var fs = require("fs");
// Read in our test file
var testfile = process.argv[2] || "filetest.js";
-var enc = process.argv[3] || 'binary';
+var enc = process.argv[3];
var data = fs.readFileSync(testfile, enc);
sys.puts("Got : " + data.length);
@@ -18,25 +18,25 @@ gzip.init();
// Pump data to be compressed
var gzdata = gzip.deflate(data, enc); // Do this as many times as required
-sys.puts("Compressed size : " + gzdata.length);
-fs.writeSync(fd, gzdata, null, "binary");
+sys.puts("Compressed chunk size : " + gzdata.length);
+fs.writeSync(fd, gzdata, 0, gzdata.length, null);
// Get the last bit
var gzlast = gzip.end();
-sys.puts("Last bit : " + gzlast.length);
-fs.writeSync(fd, gzlast, null, "binary");
+sys.puts("Compressed chunk size: " + gzlast.length);
+fs.writeSync(fd, gzlast, 0, gzlast.length, null);
fs.closeSync(fd);
sys.puts("File closed");
// See if we can uncompress it ok
var gunzip = new compress.Gunzip;
-gunzip.init();
-var testdata = fs.readFileSync(testfile + ".gz", "binary");
+gunzip.init({encoding: enc});
+var testdata = fs.readFileSync(testfile + ".gz");
sys.puts("Test opened : " + testdata.length);
var inflated = gunzip.inflate(testdata, enc);
sys.puts("GZ.inflate.length: " + inflated.length);
-sys.puts("GZ.end.length: " + gunzip.end().length);
+gunzip.end(); // no return value
if (data.length != inflated.length) {
- sys.puts('error! input/output lengths do not match');
+ sys.puts('error! input/output string lengths do not match');
}
View
2 wscript
@@ -5,7 +5,7 @@ from os.path import exists
srcdir = "."
blddir = "build"
-VERSION = "0.0.2"
+VERSION = "0.1.0"
OSTYPE = platform.system()
def set_options(opt):

0 comments on commit a3dfc78

Please sign in to comment.
Something went wrong with that request. Please try again.