/** * Convert a ClientKeyBlock to a Bucket. If an error occurs, report it via onFailure and return * null. */ protected Bucket extract( ClientKeyBlock block, Object token, ObjectContainer container, ClientContext context) { Bucket data; try { data = block.decode( context.getBucketFactory(persistent), (int) (Math.min(ctx.maxOutputLength, Integer.MAX_VALUE)), false); } catch (KeyDecodeException e1) { if (Logger.shouldLog(Logger.MINOR, this)) Logger.minor(this, "Decode failure: " + e1, e1); onFailure( new FetchException(FetchException.BLOCK_DECODE_ERROR, e1.getMessage()), token, container, context); return null; } catch (TooBigException e) { onFailure( new FetchException(FetchException.TOO_BIG, e.getMessage()), token, container, context); return null; } catch (IOException e) { Logger.error(this, "Could not capture data - disk full?: " + e, e); onFailure(new FetchException(FetchException.BUCKET_ERROR, e), token, container, context); return null; } if (Logger.shouldLog(Logger.MINOR, this)) Logger.minor( this, data == null ? "Could not decode: null" : ("Decoded " + data.size() + " bytes")); return data; }
public void testDecompressException() throws IOException { // build 5k array byte[] uncompressedData = new byte[5 * 1024]; for (int i = 0; i < uncompressedData.length; i++) { uncompressedData[i] = 1; } byte[] compressedData = doCompress(uncompressedData); Bucket inBucket = new ArrayBucket(compressedData); NullBucket outBucket = new NullBucket(); InputStream decompressorInput = null; OutputStream decompressorOutput = null; try { decompressorInput = inBucket.getInputStream(); decompressorOutput = outBucket.getOutputStream(); Compressor.COMPRESSOR_TYPE.GZIP.decompress( decompressorInput, decompressorOutput, 4096 + 10, 4096 + 20); decompressorInput.close(); decompressorOutput.close(); } catch (CompressionOutputSizeException e) { // expect this return; } finally { Closer.close(decompressorInput); Closer.close(decompressorOutput); inBucket.free(); outBucket.free(); } fail("did not throw expected CompressionOutputSizeException"); }
public void freeData(ObjectContainer container, boolean persistForever) { if (data != null) { if (persistForever) container.activate(data, 1); data.free(); if (persistForever) data.removeFrom(container); data = null; } if (persistForever) container.delete(this); }
@Override protected void freeData(ObjectContainer container) { Bucket data; synchronized (this) { data = returnBucket; returnBucket = null; } if (data != null) { if (persistenceType == PERSIST_FOREVER) container.activate(data, 5); data.free(); if (persistenceType == PERSIST_FOREVER) data.removeFrom(container); if (persistenceType == PERSIST_FOREVER) container.store(this); } }
@Override public void onSuccess(Object keyNum, ObjectContainer container, ClientContext context) { if (logMINOR) Logger.minor(this, "Succeeded (" + this + "): " + token); if (persistent) container.activate(parent, 1); if (parent.isCancelled()) { fail(new InsertException(InsertException.CANCELLED), container, context); return; } synchronized (this) { if (extraInserts > 0) { if (++completedInserts <= extraInserts) { if (logMINOR) Logger.minor( this, "Completed inserts " + completedInserts + " of extra inserts " + extraInserts + " on " + this); if (persistent) container.store(this); return; // Let it repeat until we've done enough inserts. It hasn't been unregistered yet. } } if (finished) { // Normal with persistence. Logger.normal(this, "Block already completed: " + this); return; } finished = true; } if (persistent) { container.store(this); container.activate(sourceData, 1); } if (freeData) { sourceData.free(); if (persistent) sourceData.removeFrom(container); sourceData = null; if (persistent) container.store(this); } parent.completedBlock(false, container, context); unregister(container, context, getPriorityClass(container)); if (persistent) container.activate(cb, 1); if (logMINOR) Logger.minor(this, "Calling onSuccess for " + cb); cb.onSuccess(this, container, context); if (persistent) container.deactivate(cb, 1); }
/** * Must be called just after construction, but within a transaction. * * @throws IdentifierCollisionException If the identifier is already in use. */ @Override void register(ObjectContainer container, boolean noTags) throws IdentifierCollisionException { if (client != null) assert (this.persistenceType == client.persistenceType); if (persistenceType != PERSIST_CONNECTION) try { client.register(this, container); } catch (IdentifierCollisionException e) { returnBucket.free(); if (persistenceType == PERSIST_FOREVER) returnBucket.removeFrom(container); throw e; } if (persistenceType != PERSIST_CONNECTION && !noTags) { FCPMessage msg = persistentTagMessage(container); client.queueClientRequestMessage(msg, 0, container); } }
@Override public void onGeneratedMetadata( Bucket metadata, BaseClientPutter state, ObjectContainer container) { Logger.error( this, "Bogus onGeneratedMetadata() on " + this + " from " + state, new Exception("error")); metadata.free(); }
@Override public Bucket createShadow() { Bucket undershadow = underlying.createShadow(); AEADCryptBucket ret = new AEADCryptBucket(undershadow, key); ret.setReadOnly(); return ret; }
private byte[] doCompress(byte[] uncompressedData) throws IOException { Bucket inBucket = new ArrayBucket(uncompressedData); BucketFactory factory = new ArrayBucketFactory(); Bucket outBucket = null; outBucket = Compressor.COMPRESSOR_TYPE.GZIP.compress(inBucket, factory, 32768, 32768); InputStream in = null; in = outBucket.getInputStream(); long size = outBucket.size(); byte[] outBuf = new byte[(int) size]; in.read(outBuf); return outBuf; }
public void removeFrom(ObjectContainer container) { container.activate(data, 1); data.removeFrom(container); container.activate(targetURI, 5); targetURI.removeFrom(container); container.delete(this); }
@Override public OutputStream getOutputStream() throws IOException { synchronized (this) { if (readOnly) throw new IOException("Read only"); } OutputStream os = underlying.getOutputStream(); return AEADOutputStream.createAES(os, key, NodeStarter.getGlobalSecureRandom()); }
public void realRemoveFrom(ObjectContainer container) { synchronized (this) { if (reallyRemoved) Logger.error(this, "Calling realRemoveFrom() twice on " + this); reallyRemoved = true; } bucket.removeFrom(container); container.delete(this); }
public synchronized CacheFetchResult getShadowBucket(FreenetURI key, boolean noFilter) { Object[] downloads = downloadsByURI.getArray(key); if (downloads == null) return null; for (Object o : downloads) { DownloadRequestStatus download = (DownloadRequestStatus) o; Bucket data = download.getDataShadow(); if (data == null) continue; if (data.size() == 0) continue; if (noFilter && download.filterData) continue; // FIXME it probably *is* worth the effort to allow this when it is overridden on the fetcher, // since the user changed the type??? if (download.overriddenDataType) continue; return new CacheFetchResult( new ClientMetadata(download.getMIMEType()), new NoFreeBucket(data), download.filterData); } return null; }
/** * process the bucket containing the main index file * * @param bucket */ private void processRequests(Bucket bucket) { try { InputStream is = bucket.getInputStream(); parse(is); is.close(); fetchStatus = FetchStatus.FETCHED; for (FindRequest req : waitingOnMainIndex) setdependencies(req); waitingOnMainIndex.clear(); } catch (Exception e) { fetchStatus = FetchStatus.FAILED; for (FindRequest findRequest : waitingOnMainIndex) { findRequest.setError(new TaskAbortException("Failure parsing " + toString(), e)); } Logger.error(this, indexuri, e); } finally { bucket.free(); } }
private void fail( InsertException e, boolean forceFatal, ObjectContainer container, ClientContext context) { synchronized (this) { if (finished) return; finished = true; } if (persistent) container.store(this); if (e.isFatal() || forceFatal) parent.fatallyFailedBlock(container, context); else parent.failedBlock(container, context); unregister(container, context, getPriorityClass(container)); if (freeData) { if (persistent) container.activate(sourceData, 1); sourceData.free(); if (persistent) sourceData.removeFrom(container); sourceData = null; if (persistent) container.store(this); } if (persistent) container.activate(cb, 1); cb.onFailure(e, this, container, context); }
protected void onSuccess( Bucket data, boolean fromStore, Integer token, int blockNo, ClientKeyBlock block, ObjectContainer container, ClientContext context) { if (persistent) { container.activate(this, 1); container.activate(segment, 1); container.activate(parent, 1); } if (parent.isCancelled()) { data.free(); if (persistent) data.removeFrom(container); onFailure(new FetchException(FetchException.CANCELLED), token, container, context); return; } segment.onSuccess(data, blockNo, block, container, context, this); }
public void cancel(ObjectContainer container, ClientContext context) { synchronized (this) { if (finished) return; finished = true; } boolean wasActive = true; if (persistent) { container.store(this); wasActive = container.ext().isActive(cb); if (!wasActive) container.activate(cb, 1); container.activate(sourceData, 1); } if (freeData) { sourceData.free(); if (persistent) sourceData.removeFrom(container); sourceData = null; if (persistent) container.store(this); } super.unregister(container, context, getPriorityClass(container)); cb.onFailure(new InsertException(InsertException.CANCELLED), this, container, context); if (!wasActive) container.deactivate(cb, 1); }
public void objectOnActivate(ObjectContainer container) { // StackTraceElement[] elements = Thread.currentThread().getStackTrace(); // if(elements != null && elements.length > 100) { // System.err.println("Infinite recursion in progress..."); // } if (logMINOR) Logger.minor(this, "Activating " + super.toString() + " : " + bucket.getClass()); if (bucket == this) { Logger.error(this, "objectOnActivate on DelayedFreeBucket: wrapping self!!!"); return; } // Cascading activation of dependancies container.activate(bucket, 1); }
private BlockItem getBlockItem(ObjectContainer container, ClientContext context) { try { synchronized (this) { if (finished) return null; } if (persistent) { if (sourceData == null) { Logger.error( this, "getBlockItem(): sourceData = null but active = " + container.ext().isActive(this)); return null; } } boolean deactivateBucket = false; if (persistent) { container.activate(uri, 1); deactivateBucket = !container.ext().isActive(sourceData); if (deactivateBucket) container.activate(sourceData, 1); } Bucket data = sourceData.createShadow(); FreenetURI u = uri; if (u.getKeyType().equals("CHK") && !persistent) u = FreenetURI.EMPTY_CHK_URI; else u = u.clone(); if (data == null) { data = context.tempBucketFactory.makeBucket(sourceData.size()); BucketTools.copy(sourceData, data); } if (persistent) { if (deactivateBucket) container.deactivate(sourceData, 1); container.deactivate(uri, 1); } return new BlockItem( this, data, isMetadata, compressionCodec, sourceLength, u, hashCode(), persistent); } catch (IOException e) { fail(new InsertException(InsertException.BUCKET_ERROR, e, null), container, context); return null; } }
/** * You have to synchronize on this <code>WoTMessageListInserter</code> and then on the <code> * WoTMessageManager</code> when using this function. */ private void insertMessageList(WoTOwnMessageList list) throws TransformerException, ParserConfigurationException, NoSuchMessageException, IOException, InsertException { Bucket tempB = mTBF.makeBucket(4096); /* TODO: set to a reasonable value */ OutputStream os = null; try { os = tempB.getOutputStream(); // This is what requires synchronization on the WoTMessageManager: While being marked as // "being inserted", message lists cannot be modified anymore, // so it must be guranteed that the "being inserted" mark does not change while we encode the // XML etc. mMessageManager.onMessageListInsertStarted(list); mXML.encode(mMessageManager, list, os); os.close(); os = null; tempB.setReadOnly(); /* We do not specifiy a ClientMetaData with mimetype because that would result in the insertion of an additional CHK */ InsertBlock ib = new InsertBlock(tempB, null, list.getInsertURI()); InsertContext ictx = mClient.getInsertContext(true); ClientPutter pu = mClient.insert( ib, false, null, false, ictx, this, RequestStarter.INTERACTIVE_PRIORITY_CLASS); addInsert(pu); tempB = null; if (logDEBUG) Logger.debug(this, "Started insert of WoTOwnMessageList at request URI " + list.getURI()); } finally { if (tempB != null) tempB.free(); Closer.close(os); } }
public void removeFrom(ObjectContainer container, ClientContext context) { if (logMINOR) Logger.minor(this, "removeFrom() on " + this); container.activate(uri, 5); uri.removeFrom(container); if (resultingURI != null) { container.activate(resultingURI, 5); resultingURI.removeFrom(container); } // cb, parent are responsible for removing themselves // ctx is passed in and unmodified - usually the ClientPutter removes it container.activate(errors, 5); errors.removeFrom(container); if (freeData && sourceData != null && container.ext().isStored(sourceData)) { Logger.error(this, "Data not removed!"); container.activate(sourceData, 1); sourceData.removeFrom(container); } container.delete(this); }
/** Handle an incoming connection. Blocking, obviously. */ public static void handle(Socket sock, ToadletContainer container, PageMaker pageMaker) { try { InputStream is = new BufferedInputStream(sock.getInputStream(), 4096); LineReadingInputStream lis = new LineReadingInputStream(is); while (true) { String firstLine = lis.readLine(32768, 128, false); // ISO-8859-1 or US-ASCII, _not_ UTF-8 if (firstLine == null) { sock.close(); return; } else if (firstLine.equals("")) { continue; } boolean logMINOR = Logger.shouldLog(Logger.MINOR, ToadletContextImpl.class); if (logMINOR) Logger.minor(ToadletContextImpl.class, "first line: " + firstLine); String[] split = firstLine.split(" "); if (split.length != 3) throw new ParseException( "Could not parse request line (split.length=" + split.length + "): " + firstLine); if (!split[2].startsWith("HTTP/1.")) throw new ParseException("Unrecognized protocol " + split[2]); URI uri; try { uri = URIPreEncoder.encodeURI(split[1]).normalize(); if (logMINOR) Logger.minor( ToadletContextImpl.class, "URI: " + uri + " path " + uri.getPath() + " host " + uri.getHost() + " frag " + uri.getFragment() + " port " + uri.getPort() + " query " + uri.getQuery() + " scheme " + uri.getScheme()); } catch (URISyntaxException e) { sendURIParseError(sock.getOutputStream(), true, e); return; } String method = split[0]; MultiValueTable<String, String> headers = new MultiValueTable<String, String>(); while (true) { String line = lis.readLine(32768, 128, false); // ISO-8859 or US-ASCII, not UTF-8 if (line == null) { sock.close(); return; } // System.out.println("Length="+line.length()+": "+line); if (line.length() == 0) break; int index = line.indexOf(':'); if (index < 0) { throw new ParseException("Missing ':' in request header field"); } String before = line.substring(0, index).toLowerCase(); String after = line.substring(index + 1); after = after.trim(); headers.put(before, after); } boolean disconnect = shouldDisconnectAfterHandled(split[2].equals("HTTP/1.0"), headers) || !container.enablePersistentConnections(); boolean allowPost = container.allowPosts(); BucketFactory bf = container.getBucketFactory(); ToadletContextImpl ctx = new ToadletContextImpl(sock, headers, bf, pageMaker, container); ctx.shouldDisconnect = disconnect; /* * copy the data into a bucket now, * before we go into the redirect loop */ Bucket data; boolean methodIsConfigurable = true; String slen = headers.get("content-length"); if (METHODS_MUST_HAVE_DATA.contains(method)) { // <method> must have data methodIsConfigurable = false; if (slen == null) { ctx.shouldDisconnect = true; ctx.sendReplyHeaders(400, "Bad Request", null, null, -1); return; } } else if (METHODS_CANNOT_HAVE_DATA.contains(method)) { // <method> can not have data methodIsConfigurable = false; if (slen != null) { ctx.shouldDisconnect = true; ctx.sendReplyHeaders(400, "Bad Request", null, null, -1); return; } } if (slen != null) { long len; try { len = Integer.parseInt(slen); if (len < 0) throw new NumberFormatException("content-length less than 0"); } catch (NumberFormatException e) { ctx.shouldDisconnect = true; ctx.sendReplyHeaders(400, "Bad Request", null, null, -1); return; } if (allowPost && ((!container.publicGatewayMode()) || ctx.isAllowedFullAccess())) { data = bf.makeBucket(len); BucketTools.copyFrom(data, is, len); } else { FileUtil.skipFully(is, len); if (method.equals("POST")) { ctx.sendMethodNotAllowed("POST", true); } else { sendError( sock.getOutputStream(), 403, "Forbidden", "Content not allowed in this configuration", true, null); } ctx.close(); return; } } else { // we're not doing to use it, but we have to keep // the compiler happy data = null; } if (!container.enableExtendedMethodHandling()) { if (!METHODS_RESTRICTED_MODE.contains(method)) { sendError( sock.getOutputStream(), 403, "Forbidden", "Method not allowed in this configuration", true, null); return; } } // Handle it. try { boolean redirect = true; while (redirect) { // don't go around the loop unless set explicitly redirect = false; Toadlet t; try { t = container.findToadlet(uri); } catch (PermanentRedirectException e) { Toadlet.writePermanentRedirect(ctx, "Found elsewhere", e.newuri.toASCIIString()); break; } if (t == null) { ctx.sendNoToadletError(ctx.shouldDisconnect); break; } // if the Toadlet does not support the method, we don't need to parse the data // also due this pre check a 'NoSuchMethodException' should never appear if (!(t.findSupportedMethods().contains(method))) { ctx.sendMethodNotAllowed(method, ctx.shouldDisconnect); break; } HTTPRequestImpl req = new HTTPRequestImpl(uri, data, ctx, method); try { String methodName = "handleMethod" + method; try { Class<? extends Toadlet> c = t.getClass(); Method m = c.getMethod(methodName, HANDLE_PARAMETERS); if (methodIsConfigurable) { AllowData anno = m.getAnnotation(AllowData.class); if (anno == null) { if (data != null) { sendError( sock.getOutputStream(), 400, "Bad Request", "Content not allowed", true, null); ctx.close(); return; } } else if (anno.value()) { if (data == null) { sendError( sock.getOutputStream(), 400, "Bad Request", "Missing Content", true, null); ctx.close(); return; } } } ctx.setActiveToadlet(t); Object arglist[] = new Object[] {uri, req, ctx}; m.invoke(t, arglist); } catch (InvocationTargetException ite) { throw ite.getCause(); } } catch (RedirectException re) { uri = re.newuri; redirect = true; } finally { req.freeParts(); } } if (ctx.shouldDisconnect) { sock.close(); return; } } finally { if (data != null) data.free(); } } } catch (ParseException e) { try { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("parseErrorWithError", "error", e.getMessage()), true, null); } catch (IOException e1) { // Ignore } } catch (TooLongException e) { try { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("headersLineTooLong"), true, null); } catch (IOException e1) { // Ignore } } catch (IOException e) { // ignore and return } catch (ToadletContextClosedException e) { Logger.error( ToadletContextImpl.class, "ToadletContextClosedException while handling connection!"); } catch (Throwable t) { Logger.error(ToadletContextImpl.class, "Caught error: " + t + " handling socket", t); try { sendError(sock.getOutputStream(), 500, "Internal Error", t.toString(), true, null); } catch (IOException e1) { // ignore and return } } }
/** Handle an incoming connection. Blocking, obviously. */ public static void handle(Socket sock, ToadletContainer container, PageMaker pageMaker) { try { InputStream is = new BufferedInputStream(sock.getInputStream(), 4096); LineReadingInputStream lis = new LineReadingInputStream(is); while (true) { String firstLine = lis.readLine(32768, 128, false); // ISO-8859-1 or US-ASCII, _not_ UTF-8 if (firstLine == null) { sock.close(); return; } else if (firstLine.equals("")) { continue; } boolean logMINOR = Logger.shouldLog(Logger.MINOR, ToadletContextImpl.class); if (logMINOR) Logger.minor(ToadletContextImpl.class, "first line: " + firstLine); String[] split = firstLine.split(" "); if (split.length != 3) throw new ParseException( "Could not parse request line (split.length=" + split.length + "): " + firstLine); if (!split[2].startsWith("HTTP/1.")) throw new ParseException("Unrecognized protocol " + split[2]); URI uri; try { uri = URIPreEncoder.encodeURI(split[1]).normalize(); if (logMINOR) Logger.minor( ToadletContextImpl.class, "URI: " + uri + " path " + uri.getPath() + " host " + uri.getHost() + " frag " + uri.getFragment() + " port " + uri.getPort() + " query " + uri.getQuery() + " scheme " + uri.getScheme()); } catch (URISyntaxException e) { sendURIParseError(sock.getOutputStream(), true, e); return; } String method = split[0]; MultiValueTable<String, String> headers = new MultiValueTable<String, String>(); while (true) { String line = lis.readLine(32768, 128, false); // ISO-8859 or US-ASCII, not UTF-8 if (line == null) { sock.close(); return; } // System.out.println("Length="+line.length()+": "+line); if (line.length() == 0) break; int index = line.indexOf(':'); if (index < 0) { throw new ParseException("Missing ':' in request header field"); } String before = line.substring(0, index).toLowerCase(); String after = line.substring(index + 1); after = after.trim(); headers.put(before, after); } boolean disconnect = shouldDisconnectAfterHandled(split[2].equals("HTTP/1.0"), headers) || !container.enablePersistentConnections(); boolean allowPost = container.allowPosts(); BucketFactory bf = container.getBucketFactory(); ToadletContextImpl ctx = new ToadletContextImpl(sock, headers, bf, pageMaker, container); ctx.shouldDisconnect = disconnect; /* * if we're handling a POST, copy the data into a bucket now, * before we go into the redirect loop */ Bucket data; if (method.equals("POST")) { String slen = headers.get("content-length"); if (slen == null) { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("noContentLengthInPOST"), true, null); return; } long len; try { len = Integer.parseInt(slen); if (len < 0) throw new NumberFormatException("content-length less than 0"); } catch (NumberFormatException e) { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("cannotParseContentLengthWithError", "error", e.toString()), true, null); return; } if (allowPost && ((!container.publicGatewayMode()) || ctx.isAllowedFullAccess())) { data = bf.makeBucket(len); BucketTools.copyFrom(data, is, len); } else { FileUtil.skipFully(is, len); ctx.sendMethodNotAllowed("POST", true); ctx.close(); return; } } else { // we're not doing to use it, but we have to keep // the compiler happy data = null; } // Handle it. try { boolean redirect = true; while (redirect) { // don't go around the loop unless set explicitly redirect = false; Toadlet t; try { t = container.findToadlet(uri); } catch (PermanentRedirectException e) { Toadlet.writePermanentRedirect(ctx, "Found elsewhere", e.newuri.toASCIIString()); break; } if (t == null) { ctx.sendNoToadletError(ctx.shouldDisconnect); break; } HTTPRequestImpl req = new HTTPRequestImpl(uri, data, ctx, method); try { if (method.equals("GET")) { ctx.setActiveToadlet(t); t.handleGet(uri, req, ctx); ctx.close(); } else if (method.equals("POST")) { ctx.setActiveToadlet(t); t.handlePost(uri, req, ctx); } else { ctx.sendMethodNotAllowed(method, ctx.shouldDisconnect); ctx.close(); } } catch (RedirectException re) { uri = re.newuri; redirect = true; } finally { req.freeParts(); } } if (ctx.shouldDisconnect) { sock.close(); return; } } finally { if (data != null) data.free(); } } } catch (ParseException e) { try { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("parseErrorWithError", "error", e.getMessage()), true, null); } catch (IOException e1) { // Ignore } } catch (TooLongException e) { try { sendError( sock.getOutputStream(), 400, "Bad Request", l10n("headersLineTooLong"), true, null); } catch (IOException e1) { // Ignore } } catch (IOException e) { // ignore and return } catch (ToadletContextClosedException e) { Logger.error( ToadletContextImpl.class, "ToadletContextClosedException while handling connection!"); } catch (Throwable t) { Logger.error(ToadletContextImpl.class, "Caught error: " + t + " handling socket", t); try { sendError(sock.getOutputStream(), 500, "Internal Error", t.toString(), true, null); } catch (IOException e1) { // ignore and return } } }
@Override public void close() throws IOException { in.close(); data.free(); }
public static InputStream create(Bucket data) throws IOException { return new ReadBucketAndFreeInputStream(data.getInputStream(), data); }
@Override public void onGeneratedMetadata( Bucket metadata, BaseClientPutter state, ObjectContainer container) { metadata.free(); throw new UnsupportedOperationException(); }
@Override public long size() { return underlying.size() - OVERHEAD; }
@Override public void removeFrom(ObjectContainer container) { underlying.removeFrom(container); container.delete(this); }
@Override public void storeTo(ObjectContainer container) { underlying.storeTo(container); container.store(this); }
@Override public void free() { underlying.free(); }