/************************************************************************ Copyright (c) 2001 - 2002 Microsoft Corporation Module Name : uploader.cpp Abstract : Implements HTTP upload and upload-reply transactions. Author : Jeff Roberts ***********************************************************************/ #include "stdafx.h" #include "uploader.tmh" void CProgressiveDL::Upload( CUploadJob * job, ITransferCallback * Callbacks, HANDLE Token, QMErrInfo & ErrInfo ) { try { ErrInfo.Clear(); ErrInfo.result = QM_IN_PROGRESS; THROW_HRESULT( CheckLanManHashDisabled()); CNestedImpersonation imp( Token ); Uploader uploader( this, m_Network, job, Token, Callbacks, ErrInfo ); uploader.Transfer(); } catch ( ComError err ) { if (err.m_error == S_FALSE) { // abort detected while taking the global lock. // ErrInfo.result = QM_FILE_ABORTED; } else { if (!ErrInfo.IsSet()) { Uploader::SetResult( ErrInfo, SOURCE_HTTP_CLIENT_CONN, ERROR_STYLE_HRESULT, err.Error() ); } if (ErrInfo.result == QM_IN_PROGRESS) { ErrInfo.result = CategorizeError( ErrInfo ); } } } // // Map any connection failure to BG_E_NETWORK_DISCONNECTED, if no nets are active. // if (ErrInfo.result == QM_FILE_TRANSIENT_ERROR) { if (g_Manager->m_NetworkMonitor.GetAddressCount() == 0) { ErrInfo.Set( SOURCE_HTTP_CLIENT_CONN, ERROR_STYLE_HRESULT, BG_E_NETWORK_DISCONNECTED, NULL ); } } } void Uploader::SetResult( QMErrInfo & err, ERROR_SOURCE source, ERROR_STYLE style, DWORD code, char * comment ) { err.Set( source, style, code, comment ); err.result = CategorizeError( err ); } Uploader::ResponseTable Uploader::CreateSession_ResponseTable = { GenericServerError, { { HTTP_STATUS_OK, CreateSession_NewSession }, { HTTP_STATUS_CREATED, CreateSession_NewSession }, { NULL, NULL } } }; Uploader::ResponseTable Uploader::SendData_ResponseTable = { SendData_Failure, { { HTTP_STATUS_OK, SendData_Success }, { HTTP_STATUS_RANGE_NOT_SATISFIABLE, SendData_Success }, { NULL, NULL } } }; Uploader::ResponseTable Uploader::CancelSession_ResponseTable = { CancelSession_Failure, { { HTTP_STATUS_OK, CancelSession_Success }, { NULL, NULL } } }; Uploader::ResponseTable Uploader::CloseSession_ResponseTable = { CloseSession_Failure, { { HTTP_STATUS_OK, CloseSession_Success }, { NULL, NULL } } }; Uploader::Uploader( Downloader * dl, CNetworkInterface & net, CUploadJob * job, HANDLE Token, ITransferCallback * Callbacks, QMErrInfo & ErrorInfo ) : m_Network( net ), m_Token( Token ), m_Callbacks( Callbacks ), m_Credentials( &job->QueryCredentialsList() ), m_job( job ), m_file( job->GetUploadFile() ), m_data( job->GetUploadData() ), m_JobType( job->GetType() ), m_ErrorInfo( ErrorInfo ), m_Downloader( dl ), m_Restarts( 0 ) { m_ErrorInfo.Clear(); // // Open the local file. // auto_HANDLE hFile; try { hFile = m_file->OpenLocalFileForUpload(); } catch ( ComError err ) { ErrorInfo.Set( SOURCE_QMGR_FILE, ERROR_STYLE_HRESULT, err.Error() ); throw; } // // Create a connection to the server and init the network object. // m_UrlInfo = ContactServer(); UINT64 BytesRemaining = m_file->_GetBytesTotal() - m_file->_GetBytesTransferred(); m_Network.CalculateIntervalAndBlockSize( BytesRemaining ); // // Success: now the object owns the file handle. // m_hFile = hFile.release(); } Uploader::~Uploader() { CloseHandle( m_hFile ); } void Uploader::Transfer() { bool fRetry; do { fRetry = false; try { while (!m_ErrorInfo.IsSet() && !InTerminalState()) { if (m_Callbacks->PollAbort()) { throw ComError( S_FALSE ); } switch (m_data.State) { case UPLOAD_STATE_CREATE_SESSION: CreateSession(); break; case UPLOAD_STATE_SEND_DATA: SendData(); break; case UPLOAD_STATE_GET_REPLY: GetReply(); break; case UPLOAD_STATE_CLOSE_SESSION: CloseSession(); break; case UPLOAD_STATE_CANCEL_SESSION: CancelSession(); break; } } } catch (ComError err ) { if (err.Error() == E_RETRY) { fRetry = true; } else { throw; } } } while ( fRetry ); } void Uploader::AnalyzeResponse( CBitsCommandRequest & request, DWORD result, ResponseTable & table ) { ResponseEntry * entry = table.Entries; LogDl( "HTTP status %d", result ); while (entry->Fn != NULL) { if (result == entry->Code) { (this->*(entry->Fn))( request, result ); return; } ++entry; } (this->*(table.DefaultFn))( request, result ); } void Uploader::CreateSession() { CBitsCommandRequest Request( m_UrlInfo.get() ); Request.AddPacketType( L"Create-Session" ); Request.AddContentName( m_file->GetLocalName() ); Request.AddSupportedProtocols(); DWORD result = Request.Send(); AnalyzeResponse( Request, result, CreateSession_ResponseTable ); } void Uploader::CreateSession_NewSession( CBitsCommandRequest & request, DWORD result ) { // new upload established. THROW_HRESULT( request.GetProtocol( &m_data.Protocol )); THROW_HRESULT( request.CheckResponseProtocol( &m_data.Protocol )); THROW_HRESULT( request.GetSessionId( &m_data.SessionId )); THROW_HRESULT( request.GetHostId( &m_data.HostId ) ); THROW_HRESULT( request.GetHostIdFallbackTimeout( &m_data.HostIdFallbackTimeout ) ); if ( m_data.HostId.Size() ) { m_UrlInfo = ContactServer(); } SetState( UPLOAD_STATE_SEND_DATA ); m_file->SetBytesTransferred( 0 ); } auto_ptr Uploader::ContactServer() { bool bNeedLock; try { ReleaseWriteLock( bNeedLock ); // // Open the remote file. // auto_ptr UrlInfo; UrlInfo = auto_ptr( ConnectToUrl( m_file->GetRemoteName(), &m_job->QueryProxySettings(), m_Credentials, m_data.HostId, &m_ErrorInfo )); if (!UrlInfo.get()) { ASSERT( m_ErrorInfo.IsSet()); THROW_HRESULT( E_FAIL ); } // // Ping the server to set up the HTTP connection. // CBitsCommandRequest Request( UrlInfo.get() ); Request.AddPacketType( L"Ping" ); DWORD result = Request.Send(); if (result != HTTP_STATUS_OK) { HRESULT hr; HRESULT Error; hr = Request.GetBitsError( &Error ); if (hr != S_OK && hr != BG_E_HEADER_NOT_FOUND) { THROW_HRESULT( hr ); } if (SUCCEEDED( hr )) { SetResult( m_ErrorInfo, SOURCE_HTTP_SERVER, ERROR_STYLE_HRESULT, Error ); } else { SetResult( m_ErrorInfo, SOURCE_HTTP_SERVER, ERROR_STYLE_HTTP, result ); } throw ComError( E_FAIL ); } // Update proxy and NIC info. // THROW_HRESULT( UrlInfo->GetProxyUsage( Request.Query(), &m_ErrorInfo )); THROW_HRESULT( m_Network.SetInterfaceIndex( UrlInfo.get()->fProxy ? UrlInfo.get()->ProxyHost.get() : UrlInfo.get()->HostName )); ReclaimWriteLock( bNeedLock ); return UrlInfo; } catch ( ComError err ) { ReclaimWriteLock( bNeedLock ); throw; } } void Uploader::CreateSession_InProgress( CBitsCommandRequest & request, DWORD result ) { // upload already in progress SetState( UPLOAD_STATE_SEND_DATA ); SendData_Success( request, result ); } class CFileDataReader : public CAbstractDataReader /* SendRequest() uses CAbstractDataReader to read data and rewind if a retry is necessary. CFileDataReader is the implementation used by Uploader::SendData(). It reads from an NT file handle. The handle must be seekable so that Rewind() works. */ { private: HANDLE m_hFile; LARGE_INTEGER m_OriginalOffset; DWORD m_Length; public: CFileDataReader( HANDLE hFile, UINT64 Offset, DWORD Length ) : m_hFile( hFile ), m_Length( Length ) { m_OriginalOffset.QuadPart = Offset; } virtual DWORD GetLength() const { return m_Length; } virtual HRESULT Rewind() { if (!SetFilePointerEx( m_hFile, m_OriginalOffset, NULL, FILE_BEGIN )) { return HRESULT_FROM_WIN32( GetLastError() ); } return S_OK; } virtual HRESULT Read(PVOID Buffer, DWORD Length, DWORD * pBytesRead) { if (!ReadFile( m_hFile, Buffer, Length, pBytesRead, NULL )) { DWORD s = GetLastError(); LogError("ReadFile failed %!winerr!", s); return HRESULT_FROM_WIN32( s ); } if (*pBytesRead != Length) { LogWarning("only read %d bytes of %d", *pBytesRead, Length ); } if (*pBytesRead == 0) { LogInfo("at EOF"); return S_FALSE; } return S_OK; } virtual bool IsCancelled( DWORD BytesRead ) { if (g_Manager->m_TaskScheduler.PollAbort() || g_Manager->CheckForQuantumTimeout()) { return true; } return false; } }; void Uploader::SendData() { // // If the block size is zero, we still have to send a packet in two cases: // 1. The file has length zero. // 2. The file is completely uploaded, but the server app hasn't replied yet. // if (m_Network.m_BlockSize == 0 && (m_file->_GetBytesTotal() - m_file->_GetBytesTransferred()) > 0) { m_Network.TakeSnapshot( CNetworkInterface::BLOCK_START ); m_Network.TakeSnapshot( CNetworkInterface::BLOCK_END ); } else { UINT64 FileOffset = m_file->_GetBytesTransferred(); UINT64 BodyLength = min( m_Network.m_BlockSize, m_file->_GetBytesTotal() - FileOffset ); m_ExpectedServerOffset = FileOffset + BodyLength; CFileDataReader Reader( m_hFile, FileOffset, BodyLength ); CBitsCommandRequest Request( m_UrlInfo.get() ); Request.AddSessionId( m_data.SessionId ); Request.AddPacketType( L"Fragment" ); Request.AddContentName( m_file->GetLocalName() ); Request.AddContentRange( FileOffset, FileOffset + BodyLength-1, m_file->_GetBytesTotal() ); m_Network.TakeSnapshot( CNetworkInterface::BLOCK_START ); DWORD result = Request.Send( &Reader ); m_Network.TakeSnapshot( CNetworkInterface::BLOCK_END ); AnalyzeResponse( Request, result, SendData_ResponseTable ); } // // Allow other apps to use the network for the rest of the time interval, // then take the end-of-interval snapshot. // LogInfo("waiting for end of interval"); { bool bNeedLock; try { ReleaseWriteLock( bNeedLock ); m_Network.Wait(); ReclaimWriteLock( bNeedLock ); } catch ( ComError err ) { ReclaimWriteLock( bNeedLock ); throw; } } HRESULT hr = m_Network.TakeSnapshot( CNetworkInterface::BLOCK_INTERVAL_END ); if (FAILED(hr)) { if (hr == HRESULT_FROM_WIN32( ERROR_INVALID_DATA )) { // // If the snapshot fails with ERROR_INVALID_DATA and the downloads // keep working, then our NIC has been removed and the networking // layer has silently transferred our connection to another available // NIC. We need to identify the NIC that we are now using. // LogWarning("NIC is no longer valid. Requesting retry."); hr = E_RETRY; } throw ComError( hr ); } UINT64 BytesRemaining = m_file->_GetBytesTotal() - m_file->_GetBytesTransferred(); m_Network.SetInterfaceSpeed(); m_Network.CalculateIntervalAndBlockSize( BytesRemaining ); } void Uploader::SendData_Success( CBitsCommandRequest & request, DWORD result ) { HRESULT hr; UINT64 RangeEnd; // update our notion of the server's data range hr = request.GetServerRange( &RangeEnd ); if (FAILED(hr)) { if (hr == BG_E_HEADER_NOT_FOUND) { hr = BG_E_INVALID_SERVER_RESPONSE; } throw ComError( hr ); } UINT64 Total = m_file->_GetBytesTotal(); if (RangeEnd > Total) { throw ComError( BG_E_INVALID_SERVER_RESPONSE ); } // // If the received range fails to move forward several times, then the connection is flaky and // it begins to look like an error condition. // if (RangeEnd <= m_file->_GetBytesTransferred()) { if (++m_Restarts >= 3) { throw ComError( BG_E_NO_PROGRESS ); } } m_Callbacks->UploaderProgress( RangeEnd ); // // If the server adjusted the received-range, move the file pointer to match it. // if (RangeEnd != m_ExpectedServerOffset) { LARGE_INTEGER Offset; Offset.QuadPart = RangeEnd; if (!SetFilePointerEx( m_hFile, Offset, NULL, FILE_BEGIN )) { m_ErrorInfo.Set( SOURCE_QMGR_FILE, ERROR_STYLE_HRESULT, HRESULT_FROM_WIN32( GetLastError() ) ); ThrowLastError(); } } // check for end of data upload if (RangeEnd == Total) { if (m_JobType == BG_JOB_TYPE_UPLOAD_REPLY) { CAutoString ReplyUrl; hr = request.GetReplyUrl( ReplyUrl ); if (hr == BG_E_HEADER_NOT_FOUND ) { if (result == HTTP_STATUS_RANGE_NOT_SATISFIABLE) { // // If the client didn't see a server ACK, its range may be incorrect // and the server might not send the reply URL. // So send another [zero-length] request. // } else { m_job->SetReplyFile( new CFile( m_job, BG_JOB_TYPE_DOWNLOAD, m_file->GetRemoteName(), m_job->QueryReplyFileName() )); m_job->QueryReplyFile()->SetBytesTotal( 0 ); SetState( UPLOAD_STATE_CLOSE_SESSION ); } } else if (FAILED(hr)) { throw ComError(hr); } else { // // Impersonate the user while checking file access. // CNestedImpersonation imp( m_Token ); StringHandle AbsoluteUrl = CombineUrl( m_file->GetRemoteName(), ReplyUrl.get(), 0 // flags ); m_job->SetReplyFile( new CFile( m_job, BG_JOB_TYPE_DOWNLOAD, AbsoluteUrl, m_job->QueryReplyFileName() )); SetState( UPLOAD_STATE_GET_REPLY ); } } else { SetState( UPLOAD_STATE_CLOSE_SESSION ); } } } void Uploader::SendData_Failure( CBitsCommandRequest & request, DWORD result ) { HRESULT hr; UINT64 RangeEnd; // update our notion of the server's data range hr = request.GetServerRange( &RangeEnd ); if (hr == S_OK) { UINT64 Total = m_file->_GetBytesTotal(); if (RangeEnd > Total) { throw ComError( BG_E_INVALID_SERVER_RESPONSE ); } m_Callbacks->UploaderProgress( RangeEnd ); } HRESULT Error = S_OK; hr = request.GetBitsError( &Error ); if (hr != S_OK && hr != BG_E_HEADER_NOT_FOUND) { THROW_HRESULT( hr ); } if (SUCCEEDED( hr )) { // // if the server doesn't recognize the session, retry from the beginning. // If the server resets us three times, it is probably messed up. Go into transient error state and retry later. // if (Error == BG_E_SESSION_NOT_FOUND) { if (++m_Restarts >= 3) { throw ComError( BG_E_NO_PROGRESS ); } SetState( UPLOAD_STATE_CREATE_SESSION ); m_Callbacks->UploaderProgress( 0 ); LARGE_INTEGER Offset; Offset.QuadPart = 0; if (!SetFilePointerEx( m_hFile, Offset, NULL, FILE_BEGIN )) { m_ErrorInfo.Set( SOURCE_QMGR_FILE, ERROR_STYLE_HRESULT, HRESULT_FROM_WIN32( GetLastError() ) ); ThrowLastError(); } return; } } GenericServerError( request, result ); } void Uploader::GetReply() { CFile * file = m_job->QueryReplyFile(); const PROXY_SETTINGS & ProxySettings = m_job->QueryProxySettings(); // // Make sure the file size is known; neded by the downloader. // if (file->_GetBytesTotal() == BG_SIZE_UNKNOWN) { file->DiscoverBytesTotal( m_Token, ProxySettings, m_Credentials, m_ErrorInfo); switch (m_ErrorInfo.result) { case QM_FILE_DONE: break; case QM_FILE_ABORTED: throw ComError( S_FALSE ); case QM_SERVER_FILE_CHANGED: ASSERT( 0 ); case QM_IN_PROGRESS: ASSERT( 0 ); case QM_FILE_TRANSIENT_ERROR: return; case QM_FILE_FATAL_ERROR: return; } } // // Download the reply URL. // file->Transfer( m_Token, m_job->_GetPriority(), ProxySettings, m_Credentials, m_ErrorInfo ); switch (m_ErrorInfo.result) { case QM_FILE_DONE: SetState( UPLOAD_STATE_CLOSE_SESSION ); break; case QM_FILE_ABORTED: throw ComError( S_FALSE ); case QM_SERVER_FILE_CHANGED: file->SetBytesTotal( BG_SIZE_UNKNOWN ); break; case QM_FILE_TRANSIENT_ERROR: break; case QM_FILE_FATAL_ERROR: break; case QM_IN_PROGRESS: ASSERT( 0 ); break; } } void Uploader::CloseSession() { CBitsCommandRequest Request( m_UrlInfo.get() ); Request.AddSessionId( m_data.SessionId ); Request.AddPacketType( L"Close-Session" ); Request.AddContentName( m_file->GetLocalName() ); DWORD result = Request.Send(); AnalyzeResponse( Request, result, CloseSession_ResponseTable ); } void Uploader::CloseSession_Success( CBitsCommandRequest & request, DWORD result ) { SetState( UPLOAD_STATE_CLOSED ); m_ErrorInfo.result = QM_FILE_DONE; } void Uploader::CloseSession_Failure( CBitsCommandRequest & request, DWORD result ) { // // If the session was cleaned up, we need to retry. // If the server resets us three times, it is probably messed up. Go into transient error state and retry later. // HRESULT hr; HRESULT Error = S_OK; hr = request.GetBitsError( &Error ); if (hr != S_OK && hr != BG_E_HEADER_NOT_FOUND) { THROW_HRESULT( hr ); } if (SUCCEEDED( hr )) { if (Error == BG_E_SESSION_NOT_FOUND) { if (++m_Restarts >= 3) { throw ComError( BG_E_NO_PROGRESS ); } SetState( UPLOAD_STATE_CREATE_SESSION ); m_Callbacks->UploaderProgress( 0 ); LARGE_INTEGER Offset; Offset.QuadPart = 0; if (!SetFilePointerEx( m_hFile, Offset, NULL, FILE_BEGIN )) { m_ErrorInfo.Set( SOURCE_QMGR_FILE, ERROR_STYLE_HRESULT, HRESULT_FROM_WIN32( GetLastError() ) ); ThrowLastError(); } return; } } if (result >= 500 && result <= 599) { HRESULT Error = S_OK; // temporary server error; retry later GenericServerError( request, result ); return; } // // Either success (100 and 200 series) or a permanent failure (300 and 400 series). // CloseSession_Success( request, result ); } void Uploader::CancelSession() { CBitsCommandRequest Request( m_UrlInfo.get() ); Request.AddSessionId( m_data.SessionId ); Request.AddPacketType( L"Cancel-Session" ); Request.AddContentName( m_file->GetLocalName() ); DWORD result = Request.Send(); AnalyzeResponse( Request, result, CancelSession_ResponseTable ); } void Uploader::CancelSession_Success( CBitsCommandRequest & request, DWORD result ) { SetState( UPLOAD_STATE_CANCELLED ); m_ErrorInfo.result = QM_FILE_DONE; } void Uploader::CancelSession_Failure( CBitsCommandRequest & request, DWORD result ) { if (result >= 500 && result <= 599) { HRESULT hr; HRESULT Error = S_OK; hr = request.GetBitsError( &Error ); if (hr != S_OK && hr != BG_E_HEADER_NOT_FOUND) { THROW_HRESULT( hr ); } if (SUCCEEDED( hr )) { if (Error == BG_E_SESSION_NOT_FOUND) { CancelSession_Success( request, result ); return; } } // temporary server error; retry later GenericServerError( request, result ); return; } // // Either success (100 and 200 series) or a permanent failure (300 and 400 series). // CancelSession_Success( request, result ); } void Uploader::GenericServerError( CBitsCommandRequest & request, DWORD result ) { HRESULT Error; HRESULT hr; DWORD context; ERROR_SOURCE InternalContext; // // BITS-Error-Context is optional, defaulting to REMOTE_FILE // hr = request.GetBitsErrorContext( &context ); if (hr == BG_E_HEADER_NOT_FOUND) { context = BG_ERROR_CONTEXT_REMOTE_FILE; } else if (FAILED(hr)) { throw ComError( hr ); } switch (context) { case BG_ERROR_CONTEXT_REMOTE_FILE: InternalContext = SOURCE_HTTP_SERVER; break; case BG_ERROR_CONTEXT_REMOTE_APPLICATION: InternalContext = SOURCE_HTTP_SERVER_APP; break; default: // // Don't understand; map it to something reasonable. // InternalContext = SOURCE_HTTP_SERVER; break; } // // BITS-Error is mandatory. // hr = request.GetBitsError( &Error ); if (hr == BG_E_HEADER_NOT_FOUND) { SetResult( m_ErrorInfo, InternalContext, ERROR_STYLE_HTTP, result ); return; } if (FAILED(hr)) { throw ComError( hr ); } SetResult( m_ErrorInfo, InternalContext, ERROR_STYLE_HRESULT, Error ); } FILE_DOWNLOAD_RESULT CategorizeError( QMErrInfo & ErrInfo ) { if ( ErrInfo.Style == ERROR_STYLE_HRESULT ) { switch( LONG(ErrInfo.Code) ) { case S_OK: return QM_FILE_DONE; case S_FALSE: return QM_FILE_ABORTED; // These codes indicate dynamic content or // an unsupported server so no retries are necessary. case BG_E_MISSING_FILE_SIZE: case BG_E_INSUFFICIENT_HTTP_SUPPORT: case BG_E_INSUFFICIENT_RANGE_SUPPORT: case BG_E_INVALID_SERVER_RESPONSE: case BG_E_LOCAL_FILE_CHANGED: case BG_E_TOO_LARGE: case BG_E_CLIENT_SERVER_PROTOCOL_MISMATCH: case HRESULT_FROM_WIN32( ERROR_FILE_NOT_FOUND ): case HRESULT_FROM_WIN32( ERROR_PATH_NOT_FOUND ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_ERRORS ): case HRESULT_FROM_WIN32( ERROR_INTERNET_INVALID_CA ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_CN_INVALID ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_DATE_INVALID ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_REV_FAILED ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_REVOKED ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_CERT_NO_REV ): case HRESULT_FROM_WIN32( ERROR_INTERNET_SEC_INVALID_CERT ): return QM_FILE_FATAL_ERROR; default: return QM_FILE_TRANSIENT_ERROR; } } else if (ErrInfo.Style == ERROR_STYLE_HTTP) { switch ((ErrInfo.Code / 100)) { case 1: case 2: LogError("HTTP code %u treated as an error", ErrInfo.Code); ASSERT( 0 ); return QM_FILE_TRANSIENT_ERROR; case 3: return QM_FILE_TRANSIENT_ERROR; case 4: if (ErrInfo.Code == 408 || ErrInfo.Code == 409) { return QM_FILE_TRANSIENT_ERROR; } return QM_FILE_FATAL_ERROR; case 5: default: if (ErrInfo.Code == 501) { return QM_FILE_FATAL_ERROR; } return QM_FILE_TRANSIENT_ERROR; } } else if (ErrInfo.Style == ERROR_STYLE_WIN32) { switch( ErrInfo.Code ) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: return QM_FILE_FATAL_ERROR; default: return QM_FILE_TRANSIENT_ERROR; } } else { ASSERT( 0 ); return QM_FILE_TRANSIENT_ERROR; } }