| /* |
| * Based on ../shell32/memorystream.c |
| * |
| * Copyright 1999 Juergen Schmied |
| * Copyright 2003 Mike McCormack for CodeWeavers |
| * |
| * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA |
| */ |
| |
| #include "urlmon_main.h" |
| |
| #include "winreg.h" |
| #include "winternl.h" |
| #include "wininet.h" |
| #include "shlwapi.h" |
| |
| #include "wine/debug.h" |
| |
| WINE_DEFAULT_DEBUG_CHANNEL(urlmon); |
| |
| static const IStreamVtbl stvt; |
| |
| HRESULT UMCreateStreamOnCacheFile(LPCWSTR pszURL, |
| DWORD dwSize, |
| LPWSTR pszFileName, |
| HANDLE *phfile, |
| IUMCacheStream **ppstr) |
| { |
| IUMCacheStream* ucstr; |
| HANDLE handle; |
| DWORD size; |
| LPWSTR url, c, ext = NULL; |
| HRESULT hr; |
| |
| size = (strlenW(pszURL)+1)*sizeof(WCHAR); |
| url = heap_alloc(size); |
| memcpy(url, pszURL, size); |
| |
| for (c = url; *c && *c != '#' && *c != '?'; ++c) |
| { |
| if (*c == '.') |
| ext = c+1; |
| else if(*c == '/') |
| ext = NULL; |
| } |
| |
| *c = 0; |
| |
| if(!CreateUrlCacheEntryW(url, dwSize, ext, pszFileName, 0)) |
| hr = HRESULT_FROM_WIN32(GetLastError()); |
| else |
| hr = 0; |
| |
| heap_free(url); |
| |
| if (hr) |
| return hr; |
| |
| TRACE("Opening %s\n", debugstr_w(pszFileName) ); |
| |
| handle = CreateFileW( pszFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL ); |
| if( handle == INVALID_HANDLE_VALUE ) |
| return HRESULT_FROM_WIN32(GetLastError()); |
| |
| if (phfile) |
| { |
| /* Call CreateFileW again because we need a handle with its own file pointer, and DuplicateHandle will return |
| * a handle that shares its file pointer with the original. |
| */ |
| *phfile = CreateFileW( pszFileName, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL ); |
| |
| if (*phfile == (HANDLE) HFILE_ERROR) |
| { |
| DWORD dwError = GetLastError(); |
| |
| CloseHandle(handle); |
| return HRESULT_FROM_WIN32(dwError); |
| } |
| } |
| |
| ucstr = heap_alloc_zero(sizeof(IUMCacheStream)); |
| if(ucstr) |
| { |
| ucstr->pszURL = heap_alloc_zero(sizeof(WCHAR) * (lstrlenW(pszURL) + 1)); |
| if (ucstr->pszURL) |
| { |
| ucstr->pszFileName = heap_alloc_zero(sizeof(WCHAR) * (lstrlenW(pszFileName) + 1)); |
| if (ucstr->pszFileName) |
| { |
| ucstr->lpVtbl=&stvt; |
| ucstr->ref = 1; |
| ucstr->handle = handle; |
| ucstr->closed = 0; |
| lstrcpyW(ucstr->pszURL, pszURL); |
| lstrcpyW(ucstr->pszFileName, pszFileName); |
| |
| *ppstr = ucstr; |
| |
| return S_OK; |
| } |
| heap_free(ucstr->pszURL); |
| } |
| heap_free(ucstr); |
| } |
| CloseHandle(handle); |
| if (phfile) |
| CloseHandle(*phfile); |
| return E_OUTOFMEMORY; |
| } |
| |
| void UMCloseCacheFileStream(IUMCacheStream *This) |
| { |
| if (!This->closed) |
| { |
| FILETIME ftZero; |
| |
| ftZero.dwLowDateTime = ftZero.dwHighDateTime = 0; |
| |
| This->closed = 1; |
| CommitUrlCacheEntryW(This->pszURL, |
| This->pszFileName, |
| ftZero, |
| ftZero, |
| NORMAL_CACHE_ENTRY, |
| 0, |
| 0, |
| 0, |
| 0); |
| } |
| } |
| |
| /************************************************************************** |
| * IStream_fnQueryInterface |
| */ |
| static HRESULT WINAPI IStream_fnQueryInterface(IStream *iface, |
| REFIID riid, |
| LPVOID *ppvObj) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)->(\n\tIID:\t%s,%p)\n",This,debugstr_guid(riid),ppvObj); |
| |
| *ppvObj = NULL; |
| |
| if(IsEqualIID(riid, &IID_IUnknown) || |
| IsEqualIID(riid, &IID_IStream)) |
| { |
| *ppvObj = This; |
| } |
| |
| if(*ppvObj) |
| { |
| IStream_AddRef((IStream*)*ppvObj); |
| TRACE("-- Interface: (%p)->(%p)\n",ppvObj,*ppvObj); |
| return S_OK; |
| } |
| TRACE("-- Interface: E_NOINTERFACE\n"); |
| return E_NOINTERFACE; |
| } |
| |
| /************************************************************************** |
| * IStream_fnAddRef |
| */ |
| static ULONG WINAPI IStream_fnAddRef(IStream *iface) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| ULONG refCount = InterlockedIncrement(&This->ref); |
| |
| TRACE("(%p)->(count=%u)\n", This, refCount - 1); |
| |
| return refCount; |
| } |
| |
| /************************************************************************** |
| * IStream_fnRelease |
| */ |
| static ULONG WINAPI IStream_fnRelease(IStream *iface) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| ULONG refCount = InterlockedDecrement(&This->ref); |
| |
| TRACE("(%p)->(count=%u)\n", This, refCount + 1); |
| |
| if (!refCount) |
| { |
| TRACE(" destroying UMCacheStream (%p)\n",This); |
| UMCloseCacheFileStream(This); |
| CloseHandle(This->handle); |
| heap_free(This->pszFileName); |
| heap_free(This->pszURL); |
| heap_free(This); |
| } |
| return refCount; |
| } |
| |
| static HRESULT WINAPI IStream_fnRead (IStream * iface, |
| void* pv, |
| ULONG cb, |
| ULONG* pcbRead) |
| { |
| ULONG dwBytesRead; |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)->(%p,0x%08x,%p)\n",This, pv, cb, pcbRead); |
| |
| if ( !pv ) |
| return STG_E_INVALIDPOINTER; |
| |
| if ( !pcbRead) |
| pcbRead = &dwBytesRead; |
| |
| if ( ! ReadFile( This->handle, pv, cb, pcbRead, NULL ) ) |
| return S_FALSE; |
| |
| if (!*pcbRead) |
| return This->closed ? S_FALSE : E_PENDING; |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI IStream_fnWrite (IStream * iface, |
| const void* pv, |
| ULONG cb, |
| ULONG* pcbWritten) |
| { |
| return E_NOTIMPL; |
| } |
| |
| static HRESULT WINAPI IStream_fnSeek (IStream * iface, |
| LARGE_INTEGER dlibMove, |
| DWORD dwOrigin, |
| ULARGE_INTEGER* plibNewPosition) |
| { |
| LARGE_INTEGER newpos; |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| if (!SetFilePointerEx( This->handle, dlibMove, &newpos, dwOrigin )) |
| return E_FAIL; |
| |
| if (plibNewPosition) |
| plibNewPosition->QuadPart = newpos.QuadPart; |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI IStream_fnSetSize (IStream * iface, |
| ULARGE_INTEGER libNewSize) |
| { |
| LARGE_INTEGER newpos; |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| newpos.QuadPart = libNewSize.QuadPart; |
| if( ! SetFilePointerEx( This->handle, newpos, NULL, FILE_BEGIN ) ) |
| return E_FAIL; |
| |
| if( ! SetEndOfFile( This->handle ) ) |
| return E_FAIL; |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI IStream_fnCopyTo (IStream * iface, |
| IStream* pstm, |
| ULARGE_INTEGER cb, |
| ULARGE_INTEGER* pcbRead, |
| ULARGE_INTEGER* pcbWritten) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| |
| static HRESULT WINAPI IStream_fnCommit (IStream * iface, |
| DWORD grfCommitFlags) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| |
| static HRESULT WINAPI IStream_fnRevert (IStream * iface) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| static HRESULT WINAPI IStream_fnLockRegion (IStream * iface, |
| ULARGE_INTEGER libOffset, |
| ULARGE_INTEGER cb, |
| DWORD dwLockType) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| static HRESULT WINAPI IStream_fnUnlockRegion (IStream * iface, |
| ULARGE_INTEGER libOffset, |
| ULARGE_INTEGER cb, |
| DWORD dwLockType) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| static HRESULT WINAPI IStream_fnStat (IStream * iface, |
| STATSTG* pstatstg, |
| DWORD grfStatFlag) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| static HRESULT WINAPI IStream_fnClone (IStream * iface, |
| IStream** ppstm) |
| { |
| IUMCacheStream *This = (IUMCacheStream *)iface; |
| |
| TRACE("(%p)\n",This); |
| |
| return E_NOTIMPL; |
| } |
| |
| static const IStreamVtbl stvt = |
| { |
| IStream_fnQueryInterface, |
| IStream_fnAddRef, |
| IStream_fnRelease, |
| IStream_fnRead, |
| IStream_fnWrite, |
| IStream_fnSeek, |
| IStream_fnSetSize, |
| IStream_fnCopyTo, |
| IStream_fnCommit, |
| IStream_fnRevert, |
| IStream_fnLockRegion, |
| IStream_fnUnlockRegion, |
| IStream_fnStat, |
| IStream_fnClone |
| |
| }; |
| |
| typedef struct ProxyBindStatusCallback |
| { |
| const IBindStatusCallbackVtbl *lpVtbl; |
| |
| IBindStatusCallback *pBSC; |
| } ProxyBindStatusCallback; |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_QueryInterface(IBindStatusCallback *iface, REFIID riid, void **ppv) |
| { |
| if (IsEqualGUID(&IID_IBindStatusCallback, riid) || |
| IsEqualGUID(&IID_IUnknown, riid)) |
| { |
| *ppv = iface; |
| IUnknown_AddRef(iface); |
| return S_OK; |
| } |
| |
| *ppv = NULL; |
| return E_NOINTERFACE; |
| } |
| |
| static ULONG WINAPI ProxyBindStatusCallback_AddRef(IBindStatusCallback *iface) |
| { |
| return 2; |
| } |
| |
| static ULONG WINAPI ProxyBindStatusCallback_Release(IBindStatusCallback *iface) |
| { |
| return 1; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnStartBinding(IBindStatusCallback *iface, DWORD dwReserved, |
| IBinding *pib) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnStartBinding(This->pBSC, dwReserved, pib); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_GetPriority(IBindStatusCallback *iface, LONG *pnPriority) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_GetPriority(This->pBSC, pnPriority); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnLowResource(IBindStatusCallback *iface, DWORD reserved) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnLowResource(This->pBSC, reserved); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnProgress(IBindStatusCallback *iface, ULONG ulProgress, |
| ULONG ulProgressMax, ULONG ulStatusCode, LPCWSTR szStatusText) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnProgress(This->pBSC, ulProgress, |
| ulProgressMax, ulStatusCode, |
| szStatusText); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnStopBinding(IBindStatusCallback *iface, HRESULT hresult, LPCWSTR szError) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnStopBinding(This->pBSC, hresult, szError); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_GetBindInfo(IBindStatusCallback *iface, DWORD *grfBINDF, BINDINFO *pbindinfo) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_GetBindInfo(This->pBSC, grfBINDF, pbindinfo); |
| |
| return E_INVALIDARG; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnDataAvailable(IBindStatusCallback *iface, DWORD grfBSCF, |
| DWORD dwSize, FORMATETC* pformatetc, STGMEDIUM* pstgmed) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnDataAvailable(This->pBSC, grfBSCF, dwSize, |
| pformatetc, pstgmed); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI ProxyBindStatusCallback_OnObjectAvailable(IBindStatusCallback *iface, REFIID riid, IUnknown *punk) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| |
| if(This->pBSC) |
| return IBindStatusCallback_OnObjectAvailable(This->pBSC, riid, punk); |
| |
| return S_OK; |
| } |
| |
| static HRESULT WINAPI BlockingBindStatusCallback_OnDataAvailable(IBindStatusCallback *iface, DWORD grfBSCF, |
| DWORD dwSize, FORMATETC* pformatetc, STGMEDIUM* pstgmed) |
| { |
| return S_OK; |
| } |
| |
| static const IBindStatusCallbackVtbl BlockingBindStatusCallbackVtbl = |
| { |
| ProxyBindStatusCallback_QueryInterface, |
| ProxyBindStatusCallback_AddRef, |
| ProxyBindStatusCallback_Release, |
| ProxyBindStatusCallback_OnStartBinding, |
| ProxyBindStatusCallback_GetPriority, |
| ProxyBindStatusCallback_OnLowResource, |
| ProxyBindStatusCallback_OnProgress, |
| ProxyBindStatusCallback_OnStopBinding, |
| ProxyBindStatusCallback_GetBindInfo, |
| BlockingBindStatusCallback_OnDataAvailable, |
| ProxyBindStatusCallback_OnObjectAvailable |
| }; |
| |
| static HRESULT WINAPI AsyncBindStatusCallback_GetBindInfo(IBindStatusCallback *iface, DWORD *grfBINDF, BINDINFO *pbindinfo) |
| { |
| ProxyBindStatusCallback *This = (ProxyBindStatusCallback *)iface; |
| HRESULT hr = IBindStatusCallback_GetBindInfo(This->pBSC, grfBINDF, pbindinfo); |
| *grfBINDF |= BINDF_PULLDATA | BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE; |
| return hr; |
| } |
| |
| static const IBindStatusCallbackVtbl AsyncBindStatusCallbackVtbl = |
| { |
| ProxyBindStatusCallback_QueryInterface, |
| ProxyBindStatusCallback_AddRef, |
| ProxyBindStatusCallback_Release, |
| ProxyBindStatusCallback_OnStartBinding, |
| ProxyBindStatusCallback_GetPriority, |
| ProxyBindStatusCallback_OnLowResource, |
| ProxyBindStatusCallback_OnProgress, |
| ProxyBindStatusCallback_OnStopBinding, |
| AsyncBindStatusCallback_GetBindInfo, |
| ProxyBindStatusCallback_OnDataAvailable, |
| ProxyBindStatusCallback_OnObjectAvailable |
| }; |
| |
| static HRESULT URLStartDownload(LPCWSTR szURL, LPSTREAM *ppStream, IBindStatusCallback *pBSC) |
| { |
| HRESULT hr; |
| IMoniker *pMoniker; |
| IBindCtx *pbc; |
| |
| *ppStream = NULL; |
| |
| hr = CreateURLMoniker(NULL, szURL, &pMoniker); |
| if (FAILED(hr)) |
| return hr; |
| |
| hr = CreateBindCtx(0, &pbc); |
| if (FAILED(hr)) |
| { |
| IMoniker_Release(pMoniker); |
| return hr; |
| } |
| |
| hr = RegisterBindStatusCallback(pbc, pBSC, NULL, 0); |
| if (FAILED(hr)) |
| { |
| IBindCtx_Release(pbc); |
| IMoniker_Release(pMoniker); |
| return hr; |
| } |
| |
| hr = IMoniker_BindToStorage(pMoniker, pbc, NULL, &IID_IStream, (void **)ppStream); |
| |
| /* BindToStorage returning E_PENDING because it's asynchronous is not an error */ |
| if (hr == E_PENDING) hr = S_OK; |
| |
| IBindCtx_Release(pbc); |
| IMoniker_Release(pMoniker); |
| |
| return hr; |
| } |
| |
| /*********************************************************************** |
| * URLOpenBlockingStreamA (URLMON.@) |
| */ |
| HRESULT WINAPI URLOpenBlockingStreamA(LPUNKNOWN pCaller, LPCSTR szURL, |
| LPSTREAM *ppStream, DWORD dwReserved, |
| LPBINDSTATUSCALLBACK lpfnCB) |
| { |
| LPWSTR szURLW; |
| int len; |
| HRESULT hr; |
| |
| TRACE("(%p, %s, %p, 0x%x, %p)\n", pCaller, szURL, ppStream, dwReserved, lpfnCB); |
| |
| if (!szURL || !ppStream) |
| return E_INVALIDARG; |
| |
| len = MultiByteToWideChar(CP_ACP, 0, szURL, -1, NULL, 0); |
| szURLW = heap_alloc(len * sizeof(WCHAR)); |
| if (!szURLW) |
| { |
| *ppStream = NULL; |
| return E_OUTOFMEMORY; |
| } |
| MultiByteToWideChar(CP_ACP, 0, szURL, -1, szURLW, len); |
| |
| hr = URLOpenBlockingStreamW(pCaller, szURLW, ppStream, dwReserved, lpfnCB); |
| |
| heap_free(szURLW); |
| |
| return hr; |
| } |
| |
| /*********************************************************************** |
| * URLOpenBlockingStreamW (URLMON.@) |
| */ |
| HRESULT WINAPI URLOpenBlockingStreamW(LPUNKNOWN pCaller, LPCWSTR szURL, |
| LPSTREAM *ppStream, DWORD dwReserved, |
| LPBINDSTATUSCALLBACK lpfnCB) |
| { |
| ProxyBindStatusCallback blocking_bsc; |
| |
| TRACE("(%p, %s, %p, 0x%x, %p)\n", pCaller, debugstr_w(szURL), ppStream, |
| dwReserved, lpfnCB); |
| |
| if (!szURL || !ppStream) |
| return E_INVALIDARG; |
| |
| blocking_bsc.lpVtbl = &BlockingBindStatusCallbackVtbl; |
| blocking_bsc.pBSC = lpfnCB; |
| |
| return URLStartDownload(szURL, ppStream, (IBindStatusCallback *)&blocking_bsc); |
| } |
| |
| /*********************************************************************** |
| * URLOpenStreamA (URLMON.@) |
| */ |
| HRESULT WINAPI URLOpenStreamA(LPUNKNOWN pCaller, LPCSTR szURL, DWORD dwReserved, |
| LPBINDSTATUSCALLBACK lpfnCB) |
| { |
| LPWSTR szURLW; |
| int len; |
| HRESULT hr; |
| |
| TRACE("(%p, %s, 0x%x, %p)\n", pCaller, szURL, dwReserved, lpfnCB); |
| |
| if (!szURL) |
| return E_INVALIDARG; |
| |
| len = MultiByteToWideChar(CP_ACP, 0, szURL, -1, NULL, 0); |
| szURLW = heap_alloc(len * sizeof(WCHAR)); |
| if (!szURLW) |
| return E_OUTOFMEMORY; |
| MultiByteToWideChar(CP_ACP, 0, szURL, -1, szURLW, len); |
| |
| hr = URLOpenStreamW(pCaller, szURLW, dwReserved, lpfnCB); |
| |
| heap_free(szURLW); |
| |
| return hr; |
| } |
| |
| /*********************************************************************** |
| * URLOpenStreamW (URLMON.@) |
| */ |
| HRESULT WINAPI URLOpenStreamW(LPUNKNOWN pCaller, LPCWSTR szURL, DWORD dwReserved, |
| LPBINDSTATUSCALLBACK lpfnCB) |
| { |
| HRESULT hr; |
| ProxyBindStatusCallback async_bsc; |
| IStream *pStream; |
| |
| TRACE("(%p, %s, 0x%x, %p)\n", pCaller, debugstr_w(szURL), dwReserved, |
| lpfnCB); |
| |
| if (!szURL) |
| return E_INVALIDARG; |
| |
| async_bsc.lpVtbl = &AsyncBindStatusCallbackVtbl; |
| async_bsc.pBSC = lpfnCB; |
| |
| hr = URLStartDownload(szURL, &pStream, (IBindStatusCallback *)&async_bsc); |
| if (SUCCEEDED(hr) && pStream) |
| IStream_Release(pStream); |
| |
| return hr; |
| } |