diff --git a/src/main/java/jcifs/Configuration.java b/src/main/java/jcifs/Configuration.java index d316575..e049c06 100644 --- a/src/main/java/jcifs/Configuration.java +++ b/src/main/java/jcifs/Configuration.java @@ -787,4 +787,17 @@ public interface Configuration { * @return whether to permit guest logins when user authentication is requested */ boolean isAllowGuestFallback (); + + + /** + * Property jcifs.smb.client.symlinkBehavior, defaults to THROW + * + * See {@link SymlinkBehavior} + * + * Currently, automatic symlink following is only supported when using + * SMB 3.1.1 and only for symlinks pointing to the same share. + * + * @return how to handle encountered symlinks + */ + SymlinkBehavior getSymlinkBehavior(); } diff --git a/src/main/java/jcifs/SymlinkBehavior.java b/src/main/java/jcifs/SymlinkBehavior.java new file mode 100644 index 0000000..6e79049 --- /dev/null +++ b/src/main/java/jcifs/SymlinkBehavior.java @@ -0,0 +1,35 @@ +/* + * © 2023 Moritz Bechler + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package jcifs; + +/** + * Symlink behavior + * + */ +public enum SymlinkBehavior { + /** + * Will throw a {@link jcifs.smb.SmbSymlinkException} containing the target information when + * encountering a symlink + */ + THROW, + + /** + * Automatically resolve symlinks that are located on the same share + */ + RESOLVE_SAME_SHARE +} diff --git a/src/main/java/jcifs/config/BaseConfiguration.java b/src/main/java/jcifs/config/BaseConfiguration.java index b8d9cd1..0fc4699 100644 --- a/src/main/java/jcifs/config/BaseConfiguration.java +++ b/src/main/java/jcifs/config/BaseConfiguration.java @@ -32,15 +32,10 @@ import java.util.StringTokenizer; import java.util.TimeZone; +import jcifs.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jcifs.CIFSException; -import jcifs.Configuration; -import jcifs.DialectVersion; -import jcifs.ResolverType; -import jcifs.SmbConstants; - /** * @author mbechler @@ -140,7 +135,7 @@ public class BaseConfiguration implements Configuration { protected String guestUsername = "GUEST"; protected String guestPassword = ""; protected boolean allowGuestFallback = false; - + protected SymlinkBehavior symlinkBehavior = SymlinkBehavior.THROW; /** * @throws CIFSException @@ -621,6 +616,11 @@ public boolean isAllowGuestFallback () { } + @Override + public SymlinkBehavior getSymlinkBehavior() { + return this.symlinkBehavior; + } + @Override public byte[] getMachineId () { return this.machineId; diff --git a/src/main/java/jcifs/config/DelegatingConfiguration.java b/src/main/java/jcifs/config/DelegatingConfiguration.java index 4df9f7c..72f9801 100644 --- a/src/main/java/jcifs/config/DelegatingConfiguration.java +++ b/src/main/java/jcifs/config/DelegatingConfiguration.java @@ -26,6 +26,7 @@ import jcifs.Configuration; import jcifs.DialectVersion; import jcifs.ResolverType; +import jcifs.SymlinkBehavior; /** @@ -914,4 +915,9 @@ public String getGuestPassword () { public boolean isAllowGuestFallback () { return this.delegate.isAllowGuestFallback(); } + + @Override + public SymlinkBehavior getSymlinkBehavior() { + return this.delegate.getSymlinkBehavior(); + } } diff --git a/src/main/java/jcifs/config/PropertyConfiguration.java b/src/main/java/jcifs/config/PropertyConfiguration.java index a7c7b58..d89d00b 100644 --- a/src/main/java/jcifs/config/PropertyConfiguration.java +++ b/src/main/java/jcifs/config/PropertyConfiguration.java @@ -21,11 +21,7 @@ import java.net.InetAddress; import java.util.Properties; -import jcifs.CIFSException; -import jcifs.Config; -import jcifs.Configuration; -import jcifs.DialectVersion; -import jcifs.SmbConstants; +import jcifs.*; /** @@ -145,6 +141,8 @@ public PropertyConfiguration ( Properties p ) throws CIFSException { this.guestUsername = p.getProperty("jcifs.smb.client.guestUsername", "JCIFSGUEST"); this.guestPassword = p.getProperty("jcifs.smb.client.guestPassword", ""); + this.symlinkBehavior = SymlinkBehavior.valueOf(p.getProperty("jcifs.smb.client.symlinkBehavior", "THROW")); + String minVer = p.getProperty("jcifs.smb.client.minVersion"); String maxVer = p.getProperty("jcifs.smb.client.maxVersion"); diff --git a/src/main/java/jcifs/internal/smb2/Smb2ErrorContextResponse.java b/src/main/java/jcifs/internal/smb2/Smb2ErrorContextResponse.java index 81ec41f..9796970 100644 --- a/src/main/java/jcifs/internal/smb2/Smb2ErrorContextResponse.java +++ b/src/main/java/jcifs/internal/smb2/Smb2ErrorContextResponse.java @@ -64,34 +64,6 @@ protected int readErrorContextResponse ( byte[] buffer ) { return bufferIndex; } - /** - * 2.2.2.2.1 Symbolic Link Error Response - * - * The Symbolic Link Error Response is used to indicate that a symbolic link was encountered on - * create; it describes the target path that the client MUST use if it requires to follow the - * symbolic link. This structure is contained in the ErrorData section of the SMB2 ERROR - * Response (section 2.2.2). This structure MUST NOT be returned in an SMB2 ERROR Response - * unless the Status code in the header of that response is set to STATUS_STOPPED_ON_SYMLINK. - * - * @param buffer - * @return The length, in bytes, of the response - * @throws Smb2ProtocolDecodingException - */ - public abstract int readSymLinkErrorResponse ( byte[] buffer ) throws SMBProtocolDecodingException; - /** - * 2.2.2.2.2 Share Redirect Error Context Response - * - * Servers which negotiate SMB 3.1.1 or higher can return this error context to a client in - * response to a tree connect request with the SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER bit set - * in the Flags field of the SMB2 TREE_CONNECT request. The corresponding Status code in the - * SMB2 header of the response MUST be set to STATUS_BAD_NETWORK_NAME. The error context data is - * formatted as follows. - * - * @param buffer - * @return The length, in bytes, of the response - * @throws SMBProtocolDecodingException - */ - public abstract int readShareRedirectErrorContextResponse ( byte[] buffer ) throws SMBProtocolDecodingException; } diff --git a/src/main/java/jcifs/internal/smb2/Smb2ErrorDataFormat.java b/src/main/java/jcifs/internal/smb2/Smb2ErrorDataFormat.java index 945f051..0eb2953 100644 --- a/src/main/java/jcifs/internal/smb2/Smb2ErrorDataFormat.java +++ b/src/main/java/jcifs/internal/smb2/Smb2ErrorDataFormat.java @@ -35,7 +35,19 @@ public class Smb2ErrorDataFormat extends Smb2ErrorContextResponse { private String substituteName; private String printName; - @Override + /** + * 2.2.2.2.1 Symbolic Link Error Response + * + * The Symbolic Link Error Response is used to indicate that a symbolic link was encountered on + * create; it describes the target path that the client MUST use if it requires to follow the + * symbolic link. This structure is contained in the ErrorData section of the SMB2 ERROR + * Response (section 2.2.2). This structure MUST NOT be returned in an SMB2 ERROR Response + * unless the Status code in the header of that response is set to STATUS_STOPPED_ON_SYMLINK. + * + * @param buffer + * @return The length, in bytes, of the response + * @throws SMBProtocolDecodingException + */ public int readSymLinkErrorResponse ( byte[] buffer ) throws SMBProtocolDecodingException { int bufferIndex = super.readErrorContextResponse(buffer); if ( this.errorId != 0 ) { @@ -133,7 +145,21 @@ public String getPrintName () { } - @Override + + + /** + * 2.2.2.2.2 Share Redirect Error Context Response + * + * Servers which negotiate SMB 3.1.1 or higher can return this error context to a client in + * response to a tree connect request with the SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER bit set + * in the Flags field of the SMB2 TREE_CONNECT request. The corresponding Status code in the + * SMB2 header of the response MUST be set to STATUS_BAD_NETWORK_NAME. The error context data is + * formatted as follows. + * + * @param buffer + * @return The length, in bytes, of the response + * @throws SMBProtocolDecodingException + */ public int readShareRedirectErrorContextResponse ( byte[] buffer ) throws SMBProtocolDecodingException { throw new UnsupportedOperationException("Not implemented"); diff --git a/src/main/java/jcifs/internal/smb2/Smb2SymLinkResolver.java b/src/main/java/jcifs/internal/smb2/Smb2SymLinkResolver.java index 0852aec..5893b88 100644 --- a/src/main/java/jcifs/internal/smb2/Smb2SymLinkResolver.java +++ b/src/main/java/jcifs/internal/smb2/Smb2SymLinkResolver.java @@ -17,14 +17,19 @@ */ package jcifs.internal.smb2; -import java.nio.charset.Charset; -import java.util.List; - +import jcifs.SymlinkBehavior; +import jcifs.internal.SMBProtocolDecodingException; +import jcifs.internal.smb2.create.Smb2CreateResponse; +import jcifs.smb.SmbSymlinkException; +import jcifs.smb.SmbTreeHandleInternal; +import jcifs.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jcifs.internal.SMBProtocolDecodingException; -import jcifs.util.Strings; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Locale; /** * Defines methods to resolve a symlink target path. @@ -80,13 +85,13 @@ private String resolveSymLinkTarget ( String originalFileName ) { private String getSymLinkParsedPath ( String fileName, int unparsedPathLength ) { - byte[] fileNameBytes = fileName.getBytes(Charset.forName("UTF-16LE")); + byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_16LE); return new String(fileNameBytes, 0, fileNameBytes.length - unparsedPathLength, Charset.forName("UTF-16LE")); } private String getSymLinkUnparsedPath ( String fileName, int unparsedPathLength ) { - byte[] fileNameBytes = fileName.getBytes(Charset.forName("UTF-16LE")); + byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_16LE); return new String(fileNameBytes, fileNameBytes.length - unparsedPathLength, unparsedPathLength, Charset.forName("UTF-16LE")); } @@ -111,4 +116,40 @@ private String normalizePath ( String path ) { return Strings.join(parts, '\\'); } + public String processSymlinkError(SmbTreeHandleInternal th, Smb2CreateResponse cr) throws SMBProtocolDecodingException, SmbSymlinkException { + String targetPath = this.parseSymLinkErrorData(cr.getFileName(), cr.getErrorData()); + + if (SymlinkBehavior.THROW == th.getConfig().getSymlinkBehavior()) { + throw new SmbSymlinkException(targetPath, th.getSession().getContext()); + } + + String lcaseTgt = targetPath.toLowerCase(Locale.ROOT); + if (lcaseTgt.startsWith("\\??\\unc\\")) { + // this symlink references a UNC path, this can only be (easily) handled + // if the target resides on the share and we simply can adjust the request + // path. For more extensive support, the tree connection needs to be + // switched. + + // cut off \\??\\unc\\ prefix + targetPath = targetPath.substring(8); + String curShareUnc = th.getRemoteHostName().toLowerCase(Locale.ROOT) + + "\\" + th.getConnectedShare().toLowerCase(Locale.ROOT) + "\\"; + + if ( !targetPath.startsWith(curShareUnc)) { + // TODO: convert to URL + throw new SmbSymlinkException(targetPath, th.getSession().getContext()); + } + + // return path below share + return targetPath.substring(curShareUnc.length()); + } else if (targetPath.startsWith("\\??\\")) { + // this is a link to local file, cut off prefix \??\ + // handling is up to the user + // TODO: convert to file URL? + throw new SmbSymlinkException(targetPath.substring(4), th.getSession().getContext()); + } else { + // this is a relative symlink + return targetPath; + } + } } diff --git a/src/main/java/jcifs/smb/DirFileEntryEnumIterator2.java b/src/main/java/jcifs/smb/DirFileEntryEnumIterator2.java index 92a27aa..9a9b576 100644 --- a/src/main/java/jcifs/smb/DirFileEntryEnumIterator2.java +++ b/src/main/java/jcifs/smb/DirFileEntryEnumIterator2.java @@ -18,16 +18,7 @@ package jcifs.smb; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jcifs.CIFSException; -import jcifs.Configuration; -import jcifs.DialectVersion; -import jcifs.ResourceNameFilter; -import jcifs.SmbConstants; -import jcifs.SmbResource; -import jcifs.internal.SMBProtocolDecodingException; +import jcifs.*; import jcifs.internal.smb2.Smb2SymLinkResolver; import jcifs.internal.smb2.create.Smb2CloseRequest; import jcifs.internal.smb2.create.Smb2CreateRequest; @@ -35,6 +26,8 @@ import jcifs.internal.smb2.info.Smb2QueryDirectoryRequest; import jcifs.internal.smb2.info.Smb2QueryDirectoryResponse; import jcifs.internal.util.RecursionLimiter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** @@ -92,7 +85,7 @@ protected FileEntry open () throws CIFSException { /** * - * @param path + * @param uncPath * @return * @throws CIFSException */ @@ -125,24 +118,15 @@ private FileEntry open ( String uncPath ) throws CIFSException { // We hit a symbolic link, parse the error data and resend for the 'real' directory or file path if (cr != null && cr.isReceived() && cr.getStatus() == NtStatus.NT_STATUS_STOPPED_ON_SYMLINK) { - - if (config.getMinimumVersion() != DialectVersion.SMB311) { - throw new SMBProtocolDecodingException( - "Configuration must be set to a minimum of version SMB 3.1.1 for property " - + "'jcifs.smb.client.minVersion' to resolve symbolic link target path"); + if (!th.getSession().unwrap(SmbSessionInternal.class).getTransport().unwrap(SmbTransportInternal.class).getSelectedDialect().atLeast(DialectVersion.SMB311)) { + // symlink error context information is only available since SMB3.1.1 + // TODO: might be able to get the target information via some IOCTL for earlier versions? + throw e; } - + Smb2SymLinkResolver resolver = new Smb2SymLinkResolver(); try { - Smb2SymLinkResolver resolver = new Smb2SymLinkResolver(); - String targetPath = resolver.parseSymLinkErrorData(cr.getFileName(), cr.getErrorData()); - - if (targetPath.startsWith("\\??\\")) { - throw new SMBProtocolDecodingException("SymLink target is a local path: " + targetPath); - } else { - return open(targetPath); - } - } - catch ( CIFSException | RuntimeException e3 ) { + return open(resolver.processSymlinkError(th, cr)); + } catch ( CIFSException | RuntimeException e3 ) { log.error("Exception thrown while processing symbolic link error data", e3); e.addSuppressed(e3); } diff --git a/src/main/java/jcifs/smb/SmbFile.java b/src/main/java/jcifs/smb/SmbFile.java index b94cefd..4be0096 100644 --- a/src/main/java/jcifs/smb/SmbFile.java +++ b/src/main/java/jcifs/smb/SmbFile.java @@ -697,22 +697,20 @@ else if ( ( flags & SmbConstants.O_CREAT ) == O_CREAT ) { // We hit a symbolic link, parse the error data and resend for the 'real' directory or file path if (resp != null && resp.isReceived() && resp.getStatus() == NtStatus.NT_STATUS_STOPPED_ON_SYMLINK) { this.isSymLink = true; - - if (config.getMinimumVersion() != DialectVersion.SMB311) { - throw new SMBProtocolDecodingException( - "Configuration must be set to a minimum of version SMB 3.1.1 for property " - + "'jcifs.smb.client.minVersion' to resolve symbolic link target path"); - } - try { + if (!h.getSession().unwrap(SmbSessionInternal.class).getTransport().unwrap(SmbTransportInternal.class).getSelectedDialect().atLeast(DialectVersion.SMB311)) { + // symlink error context information is only available since SMB3.1.1 + // TODO: might be able to get the target information via some IOCTL for earlier versions? + throw e; + } Smb2SymLinkResolver resolver = new Smb2SymLinkResolver(); - String targetPath = resolver.parseSymLinkErrorData(resp.getFileName(), resp.getErrorData()); - this.symLinkTargetPath = targetPath; - - if (targetPath.startsWith("\\??\\")) { - throw new SMBProtocolDecodingException("SymLink target is a local path: " + targetPath); - } else { + try { + String targetPath = resolver.parseSymLinkErrorData(resp.getFileName(), resp.getErrorData()); + this.symLinkTargetPath = targetPath; return openUnshared(targetPath, flags, access, sharing, attrs, options); + } catch ( CIFSException | RuntimeException e3 ) { + log.error("Exception thrown while processing symbolic link error data", e3); + e.addSuppressed(e3); } } catch ( CIFSException | RuntimeException e2 ) { @@ -720,11 +718,9 @@ else if ( ( flags & SmbConstants.O_CREAT ) == O_CREAT ) { throw e2; } } - else { - log.error("Exception thrown while processing SMB2 request", e); - throw e; - } + throw e; } + } else if ( h.hasCapability(SmbConstants.CAP_NT_SMBS) ) { SmbComNTCreateAndXResponse resp = new SmbComNTCreateAndXResponse(config); @@ -1804,7 +1800,11 @@ protected T withOpen ( SmbTreeHandleImpl @SuppressWarnings ( "unchecked" ) protected T withOpen ( SmbTreeHandleImpl th, int createDisposition, int createOptions, int fileAttributes, int desiredAccess, int shareAccess, ServerMessageBlock2Request first, ServerMessageBlock2Request... others ) throws CIFSException { - Smb2CreateRequest cr = new Smb2CreateRequest(th.getConfig(), getUncPath()); + return withOpen(th, getUncPath(), createDisposition, createOptions, fileAttributes, desiredAccess, shareAccess, first, others); + } + + private T withOpen(SmbTreeHandleImpl th, String path, int createDisposition, int createOptions, int fileAttributes, int desiredAccess, int shareAccess, ServerMessageBlock2Request first,ServerMessageBlock2Request... others) throws CIFSException { + Smb2CreateRequest cr = new Smb2CreateRequest(th.getConfig(), path); try { cr.setCreateDisposition(createDisposition); cr.setCreateOptions(createOptions); @@ -1818,7 +1818,7 @@ protected T withOpen ( SmbTreeHandleImpl cr.chain(first); cur = first; - for ( ServerMessageBlock2Request req : others ) { + for ( ServerMessageBlock2Request req : others) { cur.chain(req); cur = req; } @@ -1859,15 +1859,21 @@ protected T withOpen ( SmbTreeHandleImpl if ( createResp.isReceived() && createResp.getStatus() == NtStatus.NT_STATUS_STOPPED_ON_SYMLINK ) { this.isSymLink = true; - if (th.getConfig().getMinimumVersion() != DialectVersion.SMB311) { - throw new SMBProtocolDecodingException( - "Configuration must be set to a minimum of version SMB 3.1.1 for property " - + "'jcifs.smb.client.minVersion' to resolve symbolic link target path"); - } - // we hit a symbolic link, parse the error data to retrieve the target path + if (!th.getSession().unwrap(SmbSessionInternal.class).getTransport().unwrap(SmbTransportInternal.class).getSelectedDialect().atLeast(DialectVersion.SMB311)) { + // symlink error context information is only available since SMB3.1.1 + // TODO: might be able to get the target information via some IOCTL for earlier versions? + throw e; + } Smb2SymLinkResolver resolver = new Smb2SymLinkResolver(); - this.symLinkTargetPath = resolver.parseSymLinkErrorData(createResp.getFileName(), createResp.getErrorData()); + try { + this.symLinkTargetPath = resolver.processSymlinkError(th, createResp); + // retry the request with the adjusted path + return withOpen(th, this.symLinkTargetPath, createDisposition, createOptions, fileAttributes, desiredAccess, shareAccess, first, others); + } catch ( CIFSException | RuntimeException e3 ) { + log.error("Exception thrown while processing symbolic link error data", e3); + e.addSuppressed(e3); + } } else if ( createResp.isReceived() && createResp.getStatus() == NtStatus.NT_STATUS_OK ) { // make sure that the handle is closed when one of the requests fails th.send(new Smb2CloseRequest(th.getConfig(), createResp.getFileId()), RequestParam.NO_RETRY); diff --git a/src/main/java/jcifs/smb/SmbSymlinkException.java b/src/main/java/jcifs/smb/SmbSymlinkException.java new file mode 100644 index 0000000..29ce3cd --- /dev/null +++ b/src/main/java/jcifs/smb/SmbSymlinkException.java @@ -0,0 +1,26 @@ +package jcifs.smb; + +import jcifs.CIFSContext; +import jcifs.CIFSException; +import jcifs.SmbResource; + +public class SmbSymlinkException extends SmbException { + + private final String target; + private transient final CIFSContext context; + + public SmbSymlinkException(String target, CIFSContext ctx) { + // TODO: transform to URL + this.target = target; + this.context = ctx; + } + + + public String getTarget() { + return target; + } + + public SmbResource getResource() throws CIFSException { + return this.context.get(this.target); + } +} diff --git a/src/main/java/jcifs/smb/SmbTransportImpl.java b/src/main/java/jcifs/smb/SmbTransportImpl.java index b170312..471a9d9 100644 --- a/src/main/java/jcifs/smb/SmbTransportImpl.java +++ b/src/main/java/jcifs/smb/SmbTransportImpl.java @@ -150,11 +150,6 @@ class SmbTransportImpl extends Transport implements SmbTransportInternal, SmbCon } - /** - * {@inheritDoc} - * - * @see jcifs.util.transport.Transport#getResponseTimeout() - */ @Override protected int getResponseTimeout ( Request req ) { if ( req instanceof CommonServerMessageBlockRequest ) { @@ -213,6 +208,10 @@ public boolean hasCapability ( int cap ) throws SmbException { return getNegotiateResponse().haveCapabilitiy(cap); } + @Override + public DialectVersion getSelectedDialect() throws SmbException { + return getNegotiateResponse().getSelectedDialect(); + } /** * @return the negotiated diff --git a/src/main/java/jcifs/smb/SmbTransportInternal.java b/src/main/java/jcifs/smb/SmbTransportInternal.java index 546935c..df12e16 100644 --- a/src/main/java/jcifs/smb/SmbTransportInternal.java +++ b/src/main/java/jcifs/smb/SmbTransportInternal.java @@ -20,11 +20,7 @@ import java.io.IOException; -import jcifs.CIFSContext; -import jcifs.CIFSException; -import jcifs.DfsReferralData; -import jcifs.SmbSession; -import jcifs.SmbTransport; +import jcifs.*; /** @@ -125,4 +121,11 @@ public interface SmbTransportInternal extends SmbTransport { * @return number of inflight requests */ int getInflightRequests (); + + + /** + * + * @return negotiated SMB version + */ + DialectVersion getSelectedDialect() throws SmbException; } diff --git a/src/test/java/jcifs/tests/SymLinkTest.java b/src/test/java/jcifs/tests/SymLinkTest.java index 35377a5..128e355 100644 --- a/src/test/java/jcifs/tests/SymLinkTest.java +++ b/src/test/java/jcifs/tests/SymLinkTest.java @@ -227,7 +227,15 @@ private SmbFile createSession() throws MalformedURLException, CIFSException { private long copyInputStreamToFile(InputStream is, File file) throws IOException { try (OutputStream output = new FileOutputStream(file)) { - return is.transferTo(output); + int read = 0; + long total = 0; + byte[] buffer = new byte[1024]; + + while ( (read = is.read(buffer)) != 0 ) { + output.write(buffer, 0, read); + total += read; + } + return total; } catch (IOException ioe) { log.error("copyInputStreamToFile error", ioe); throw ioe;