/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4che3.net;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.io.DicomInputStream;
import org.dcm4che3.net.pdu.AAbort;
import org.dcm4che3.net.pdu.AAssociateAC;
import org.dcm4che3.net.pdu.AAssociateRJ;
import org.dcm4che3.net.pdu.AAssociateRQ;
import org.dcm4che3.net.pdu.AAssociateRQAC;
import org.dcm4che3.net.pdu.CommonExtendedNegotiation;
import org.dcm4che3.net.pdu.ExtendedNegotiation;
import org.dcm4che3.net.pdu.PresentationContext;
import org.dcm4che3.net.pdu.RoleSelection;
import org.dcm4che3.net.pdu.UserIdentityAC;
import org.dcm4che3.net.pdu.UserIdentityRQ;
import org.dcm4che3.util.ByteUtils;
import org.dcm4che3.util.SafeClose;
import org.dcm4che3.util.StreamUtils;
/**
* @author Gunter Zeilinger <gunterze@gmail.com>
*
*/
class PDUDecoder extends PDVInputStream {
private static final String UNRECOGNIZED_PDU =
"{}: unrecognized PDU[type={}, len={}]";
private static final String INVALID_PDU_LENGTH =
"{}: invalid length of PDU[type={}, len={}]";
private static final String INVALID_COMMON_EXTENDED_NEGOTIATION =
"{}: invalid Common Extended Negotiation sub-item in PDU[type={}, len={}]";
private static final String INVALID_USER_IDENTITY =
"{}: invalid User Identity sub-item in PDU[type={}, len={}]";
private static final String INVALID_PDV =
"{}: invalid PDV in PDU[type={}, len={}]";
private static final String UNEXPECTED_PDV_TYPE =
"{}: unexpected PDV type in PDU[type={}, len={}]";
private static final String UNEXPECTED_PDV_PCID =
"{}: unexpected pcid in PDV in PDU[type={}, len={}]";
private static final int MAX_PDU_LEN = 0x1000000; // 16MiB
private final Association as;
private final InputStream in;
private final Thread th;
private byte[] buf = new byte[6 + Connection.DEF_MAX_PDU_LENGTH];
private int pos;
private int pdutype;
private int pdulen;
private int pcid = -1;
private int pdvmch;
private int pdvend;
public PDUDecoder(Association as, InputStream in) {
this.as = as;
this.in = in;
this.th = Thread.currentThread();
}
private int remaining() {
return pdulen + 6 - pos;
}
private boolean hasRemaining() {
return pos < pdulen + 6;
}
private int get() {
if (!hasRemaining())
throw new IndexOutOfBoundsException();
return buf[pos++] & 0xFF;
}
private void get(byte[] b, int off, int len) {
if (len > remaining())
throw new IndexOutOfBoundsException();
System.arraycopy(buf, pos, b, off, len);
pos += len;
}
private void skip(int len) {
if (len > remaining())
throw new IndexOutOfBoundsException();
pos += len;
}
private int getUnsignedShort() {
int val = ByteUtils.bytesToUShortBE(buf, pos);
pos += 2;
return val;
}
private int getInt() {
int val = ByteUtils.bytesToIntBE(buf, pos);
pos += 4;
return val;
}
private byte[] getBytes(int len) {
byte[] bs = new byte[len];
get(bs, 0, len);
return bs;
}
private byte[] decodeBytes() {
return getBytes(getUnsignedShort());
}
public void nextPDU() throws IOException {
checkThread();
Association.LOG.trace("{}: waiting for PDU", as);
readFully(0, 10);
pos = 0;
pdutype = get();
get();
pdulen = getInt();
Association.LOG.trace("{} >> PDU[type={}, len={}]",
new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL });
switch (pdutype) {
case PDUType.A_ASSOCIATE_RQ:
readPDU();
as.onAAssociateRQ((AAssociateRQ) decode(new AAssociateRQ()));
return;
case PDUType.A_ASSOCIATE_AC:
readPDU();
as.onAAssociateAC((AAssociateAC) decode(new AAssociateAC()));
return;
case PDUType.P_DATA_TF:
readPDU();
as.onPDataTF();
return;
case PDUType.A_ASSOCIATE_RJ:
checkPDULength(4);
get();
as.onAAssociateRJ(new AAssociateRJ(get(), get(), get()));
break;
case PDUType.A_RELEASE_RQ:
checkPDULength(4);
as.onAReleaseRQ();
break;
case PDUType.A_RELEASE_RP:
checkPDULength(4);
as.onAReleaseRP();
break;
case PDUType.A_ABORT:
checkPDULength(4);
get();
get();
as.onAAbort(new AAbort(get(), get()));
break;
default:
abort(AAbort.UNRECOGNIZED_PDU, UNRECOGNIZED_PDU);
}
}
private void checkThread() {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
}
private void checkPDULength(int len) throws AAbort {
if (pdulen != len)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
}
private void readPDU() throws IOException {
if (pdulen < 4 || pdulen > MAX_PDU_LEN)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
if (6 + pdulen > buf.length)
buf = Arrays.copyOf(buf, 6 + pdulen);
readFully(10, pdulen - 4);
}
private void readFully(int off, int len) throws IOException {
try {
StreamUtils.readFully(in, buf, off, len);
} catch (IOException e) {
throw e;
}
}
private void abort(int reason, String logmsg) throws AAbort {
Association.LOG.warn(logmsg,
new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL });
throw new AAbort(AAbort.UL_SERIVE_PROVIDER, reason);
}
@SuppressWarnings("deprecation")
private String getString(int len) {
if (pos + len > pdulen + 6)
throw new IndexOutOfBoundsException();
String s;
// Skip illegal trailing NULL
int len0 = len;
while (len0 > 0 && buf[pos + len0 - 1] == 0) {
len0--;
}
s = new String(buf, 0, pos, len0);
pos += len;
return s;
}
private String decodeString() {
return getString(getUnsignedShort());
}
private AAssociateRQAC decode(AAssociateRQAC rqac)
throws AAbort {
try {
rqac.setProtocolVersion(getUnsignedShort());
get();
get();
rqac.setCalledAET(getString(16).trim());
rqac.setCallingAET(getString(16).trim());
rqac.setReservedBytes(getBytes(32));
while (pos < pdulen)
decodeItem(rqac);
checkPDULength(pos - 6);
} catch (IndexOutOfBoundsException e) {
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
}
return rqac;
}
private void decodeItem(AAssociateRQAC rqac) throws AAbort {
int itemType = get();
get(); // skip reserved byte
int itemLen = getUnsignedShort();
switch (itemType)
{
case ItemType.APP_CONTEXT:
rqac.setApplicationContext(getString(itemLen));
break;
case ItemType.RQ_PRES_CONTEXT:
case ItemType.AC_PRES_CONTEXT:
rqac.addPresentationContext(decodePC(itemLen));
break;
case ItemType.USER_INFO:
decodeUserInfo(itemLen, rqac);
break;
default:
skip(itemLen);
}
}
private PresentationContext decodePC(int itemLen) {
int pcid = get();
get(); // skip reserved byte
int result = get();
get(); // skip reserved byte
String as = null;
ArrayList<String> tss = new ArrayList<String>(1);
int endpos = pos + itemLen - 4;
while (pos < endpos) {
int subItemType = get() & 0xff;
get(); // skip reserved byte
int subItemLen = getUnsignedShort();
switch (subItemType)
{
case ItemType.ABSTRACT_SYNTAX:
as = getString(subItemLen);
break;
case ItemType.TRANSFER_SYNTAX:
tss.add(getString(subItemLen));
break;
default:
skip(subItemLen);
}
}
return new PresentationContext(pcid, result, as,
tss.toArray(new String[tss.size()]));
}
private void decodeUserInfo(int itemLength, AAssociateRQAC rqac) throws AAbort
{
int endpos = pos + itemLength;
while (pos < endpos)
decodeUserInfoSubItem(rqac);
}
private void decodeUserInfoSubItem(AAssociateRQAC rqac) throws AAbort
{
int itemType = get();
get(); // skip reserved byte
int itemLen = getUnsignedShort();
switch (itemType)
{
case ItemType.MAX_PDU_LENGTH:
rqac.setMaxPDULength(getInt());
break;
case ItemType.IMPL_CLASS_UID:
rqac.setImplClassUID(getString(itemLen));
break;
case ItemType.ASYNC_OPS_WINDOW:
rqac.setMaxOpsInvoked(getUnsignedShort());
rqac.setMaxOpsPerformed(getUnsignedShort());
break;
case ItemType.ROLE_SELECTION:
rqac.addRoleSelection(decodeRoleSelection(itemLen));
break;
case ItemType.IMPL_VERSION_NAME:
rqac.setImplVersionName(getString(itemLen));
break;
case ItemType.EXT_NEG:
rqac.addExtendedNegotiation(decodeExtNeg(itemLen));
break;
case ItemType.COMMON_EXT_NEG:
rqac.addCommonExtendedNegotiation(decodeCommonExtNeg(itemLen));
break;
case ItemType.RQ_USER_IDENTITY:
rqac.setUserIdentityRQ(decodeUserIdentityRQ(itemLen));
break;
case ItemType.AC_USER_IDENTITY:
rqac.setUserIdentityAC(decodeUserIdentityAC(itemLen));
break;
default:
skip(itemLen);
}
}
private RoleSelection decodeRoleSelection(int itemLen) {
String cuid = decodeString();
boolean scu = get() != 0;
boolean scp = get() != 0;
return new RoleSelection(cuid, scu, scp);
}
private ExtendedNegotiation decodeExtNeg(int itemLen) {
int uidLength = getUnsignedShort();
String cuid = getString(uidLength);
byte[] info = getBytes(itemLen - uidLength - 2);
return new ExtendedNegotiation(cuid, info);
}
private CommonExtendedNegotiation decodeCommonExtNeg(int itemLen)
throws AAbort {
int endPos = pos + itemLen;
String sopCUID = getString(getUnsignedShort());
String serviceCUID = getString(getUnsignedShort());
ArrayList<String> relSopCUIDs = new ArrayList<String>(1);
int relSopCUIDsLen = getUnsignedShort();
int endRelSopCUIDs = pos + relSopCUIDsLen;
while (pos < endRelSopCUIDs)
relSopCUIDs.add(decodeString());
if (pos != endRelSopCUIDs || pos > endPos)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE,
INVALID_COMMON_EXTENDED_NEGOTIATION);
skip(endPos - pos);
return new CommonExtendedNegotiation(sopCUID, serviceCUID,
relSopCUIDs.toArray(new String[relSopCUIDs.size()]));
}
private UserIdentityRQ decodeUserIdentityRQ(int itemLen) throws AAbort {
int endPos = pos + itemLen;
int type = get() & 0xff;
boolean rspReq = get() != 0;
byte[] primaryField = decodeBytes();
byte[] secondaryField = decodeBytes();
if (pos != endPos)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY);
return new UserIdentityRQ(type, rspReq, primaryField, secondaryField);
}
private UserIdentityAC decodeUserIdentityAC(int itemLen) throws AAbort {
int endPos = pos + itemLen;
byte[] serverResponse = decodeBytes();
if (pos != endPos)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY);
return new UserIdentityAC(serverResponse);
}
public void decodeDIMSE() throws IOException {
checkThread();
if (pcid != - 1)
return; // already inside decodeDIMSE
nextPDV(PDVType.COMMAND, -1);
PresentationContext pc = as.getPresentationContext(pcid);
if (pc == null) {
Association.LOG.warn(
"{}: No Presentation Context with given ID - {}",
as, pcid);
throw new AAbort();
}
if (!pc.isAccepted()) {
Association.LOG.warn(
"{}: No accepted Presentation Context with given ID - {}",
as, pcid);
throw new AAbort();
}
Attributes cmd = readCommand();
Dimse dimse = dimseOf(cmd);
String tsuid = pc.getTransferSyntax();
if (Dimse.LOG.isInfoEnabled()) {
Dimse.LOG.info("{} >> {}", as, dimse.toString(cmd, pcid, tsuid));
Dimse.LOG.debug("Command:\n{}", cmd);
}
if (dimse == Dimse.C_CANCEL_RQ) {
as.onCancelRQ(cmd);
} else if (Commands.hasDataset(cmd)) {
nextPDV(PDVType.DATA, pcid);
if (dimse.isRSP()) {
Attributes data = readDataset(tsuid);
Dimse.LOG.debug("Dataset:\n{}", data);
as.onDimseRSP(dimse, cmd, data);
} else {
as.onDimseRQ(pc, dimse, cmd, this);
long skipped = skipAll();
if (skipped > 0)
Association.LOG.debug(
"{}: Service User did not consume {} bytes of DIMSE data.",
as, skipped);
}
skipAll();
} else {
if (dimse.isRSP()) {
as.onDimseRSP(dimse, cmd, null);
} else {
as.onDimseRQ(pc, dimse, cmd, null);
}
}
pcid = -1;
}
private Dimse dimseOf(Attributes cmd) throws AAbort {
try {
return Dimse.valueOf(cmd.getInt(Tag.CommandField, 0));
} catch (IllegalArgumentException e) {
Dimse.LOG.info("{}: illegal DIMSE:", as);
Dimse.LOG.info("\n{}", cmd);
throw new AAbort();
}
}
private Attributes readCommand() throws IOException {
DicomInputStream in =
new DicomInputStream(this, UID.ImplicitVRLittleEndian);
try {
return in.readCommand();
} finally {
SafeClose.close(in);
}
}
@Override
public Attributes readDataset(String tsuid) throws IOException {
DicomInputStream in = new DicomInputStream(this, tsuid);
try {
return in.readDataset(-1, -1);
} finally {
SafeClose.close(in);
}
}
private void nextPDV(int expectedPDVType, int expectedPCID)
throws IOException {
if (!hasRemaining()) {
nextPDU();
if (pdutype != PDUType.P_DATA_TF) {
Association.LOG.info(
"{}: Expected P-DATA-TF PDU but received PDU[type={}]",
as, pdutype);
throw new EOFException();
}
}
if (remaining() < 6)
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV);
int pdvlen = getInt();
this.pdvend = pos + pdvlen;
if (pdvlen < 2 || pdvlen > remaining())
abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV);
this.pcid = get();
this.pdvmch = get();
Association.LOG.trace("{} >> PDV[len={}, pcid={}, mch={}]",
new Object[] { as, pdvlen, pcid, pdvmch } );
if ((pdvmch & PDVType.COMMAND) != expectedPDVType)
abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_TYPE);
if (expectedPCID != -1 && pcid != expectedPCID)
abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_PCID);
}
private boolean isLastPDV() throws IOException {
while (pos == pdvend) {
if ((pdvmch & PDVType.LAST) != 0)
return true;
nextPDV(pdvmch & PDVType.COMMAND, pcid);
}
return false;
}
@Override
public int read() throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
if (isLastPDV())
return -1;
return get();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
if (isLastPDV())
return -1;
int read = Math.min(len, pdvend - pos);
get(b, off, read);
return read;
}
@Override
public final int available() {
return pdvend - pos;
}
@Override
public long skip(long n) throws IOException
{
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
if (n <= 0 || isLastPDV())
return 0;
int skipped = (int) Math.min(n, pdvend - pos);
skip(skipped);
return skipped;
}
@Override
public void close() throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
skipAll();
}
@Override
public long skipAll() throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
long n = 0;
while (!isLastPDV()) {
n += pdvend - pos;
pos = pdvend;
}
return n;
}
@Override
public void copyTo(OutputStream out, int length) throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
int remaining = length;
while (remaining > 0) {
if (isLastPDV())
throw new EOFException("remaining: " + remaining);
int read = Math.min(remaining, pdvend - pos);
out.write(buf, pos, read);
remaining -= read;
pos += read;
}
}
@Override
public void copyTo(OutputStream out) throws IOException {
if (th != Thread.currentThread())
throw new IllegalStateException("Entered by wrong thread");
while (!isLastPDV()) {
out.write(buf, pos, pdvend - pos);
pos = pdvend;
}
}
}