001/* ***** BEGIN LICENSE BLOCK *****
002 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003 *
004 * The contents of this file are subject to the Mozilla Public License Version
005 * 1.1 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 * http://www.mozilla.org/MPL/
008 *
009 * Software distributed under the License is distributed on an "AS IS" basis,
010 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
011 * for the specific language governing rights and limitations under the
012 * License.
013 *
014 * The Original Code is part of dcm4che, an implementation of DICOM(TM) in
015 * Java(TM), hosted at https://github.com/gunterze/dcm4che.
016 *
017 * The Initial Developer of the Original Code is
018 * Agfa Healthcare.
019 * Portions created by the Initial Developer are Copyright (C) 2011
020 * the Initial Developer. All Rights Reserved.
021 *
022 * Contributor(s):
023 * See @authors listed below
024 *
025 * Alternatively, the contents of this file may be used under the terms of
026 * either the GNU General Public License Version 2 or later (the "GPL"), or
027 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
028 * in which case the provisions of the GPL or the LGPL are applicable instead
029 * of those above. If you wish to allow use of your version of this file only
030 * under the terms of either the GPL or the LGPL, and not to allow others to
031 * use your version of this file under the terms of the MPL, indicate your
032 * decision by deleting the provisions above and replace them with the notice
033 * and other provisions required by the GPL or the LGPL. If you do not delete
034 * the provisions above, a recipient may use your version of this file under
035 * the terms of any one of the MPL, the GPL or the LGPL.
036 *
037 * ***** END LICENSE BLOCK ***** */
038
039package org.dcm4che3.net;
040
041import java.io.EOFException;
042import java.io.IOException;
043import java.io.InputStream;
044import java.io.OutputStream;
045import java.util.ArrayList;
046import java.util.Arrays;
047
048import org.dcm4che3.data.Tag;
049import org.dcm4che3.data.UID;
050import org.dcm4che3.data.Attributes;
051import org.dcm4che3.io.DicomInputStream;
052import org.dcm4che3.net.pdu.AAbort;
053import org.dcm4che3.net.pdu.AAssociateAC;
054import org.dcm4che3.net.pdu.AAssociateRJ;
055import org.dcm4che3.net.pdu.AAssociateRQ;
056import org.dcm4che3.net.pdu.AAssociateRQAC;
057import org.dcm4che3.net.pdu.CommonExtendedNegotiation;
058import org.dcm4che3.net.pdu.ExtendedNegotiation;
059import org.dcm4che3.net.pdu.PresentationContext;
060import org.dcm4che3.net.pdu.RoleSelection;
061import org.dcm4che3.net.pdu.UserIdentityAC;
062import org.dcm4che3.net.pdu.UserIdentityRQ;
063import org.dcm4che3.util.ByteUtils;
064import org.dcm4che3.util.SafeClose;
065import org.dcm4che3.util.StreamUtils;
066
067/**
068 * @author Gunter Zeilinger <gunterze@gmail.com>
069 *
070 */
071class PDUDecoder extends PDVInputStream {
072
073    private static final String UNRECOGNIZED_PDU =
074            "{}: unrecognized PDU[type={}, len={}]";
075    private static final String INVALID_PDU_LENGTH =
076            "{}: invalid length of PDU[type={}, len={}]";
077    private static final String INVALID_COMMON_EXTENDED_NEGOTIATION =
078            "{}: invalid Common Extended Negotiation sub-item in PDU[type={}, len={}]";
079    private static final String INVALID_USER_IDENTITY =
080            "{}: invalid User Identity sub-item in PDU[type={}, len={}]";
081    private static final String INVALID_PDV =
082            "{}: invalid PDV in PDU[type={}, len={}]";
083    private static final String UNEXPECTED_PDV_TYPE =
084            "{}: unexpected PDV type in PDU[type={}, len={}]";
085    private static final String UNEXPECTED_PDV_PCID =
086            "{}: unexpected pcid in PDV in PDU[type={}, len={}]";
087
088    private static final int MAX_PDU_LEN = 0x1000000; // 16MiB
089
090    private final Association as;
091    private final InputStream in;
092    private final Thread th;
093    private byte[] buf = new byte[6 + Connection.DEF_MAX_PDU_LENGTH];
094    private int pos;
095    private int pdutype;
096    private int pdulen;
097    private int pcid = -1;
098    private int pdvmch;
099    private int pdvend;
100
101    public PDUDecoder(Association as, InputStream in) {
102        this.as = as;
103        this.in = in;
104        this.th = Thread.currentThread();
105    }
106
107    private int remaining() {
108        return pdulen + 6 - pos;
109    }
110
111    private boolean hasRemaining() {
112        return pos < pdulen + 6;
113    }
114        
115    private int get() {
116        if (!hasRemaining())
117            throw new IndexOutOfBoundsException();
118        return buf[pos++] & 0xFF;
119    }
120
121    private void get(byte[] b, int off, int len) {
122        if (len > remaining())
123            throw new IndexOutOfBoundsException();
124        System.arraycopy(buf, pos, b, off, len);
125        pos += len;
126    }
127
128    private void skip(int len) {
129        if (len > remaining())
130            throw new IndexOutOfBoundsException();
131        pos += len;
132    }
133        
134    private int getUnsignedShort() {
135        int val = ByteUtils.bytesToUShortBE(buf, pos);
136        pos += 2;
137        return val;
138    }
139
140    private int getInt() {
141        int val = ByteUtils.bytesToIntBE(buf, pos);
142        pos += 4;
143        return val;
144    }
145
146    private byte[] getBytes(int len) {
147        byte[] bs = new byte[len];
148        get(bs, 0, len);
149        return bs;
150    }
151
152    private byte[] decodeBytes() {
153        return getBytes(getUnsignedShort());
154    }
155
156    public void nextPDU() throws IOException {
157        checkThread();
158        Association.LOG.trace("{}: waiting for PDU", as);
159        readFully(0, 10);
160        pos = 0;
161        pdutype = get();
162        get();
163        pdulen = getInt();
164        Association.LOG.trace("{} >> PDU[type={}, len={}]",
165                new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL });
166        switch (pdutype) {
167        case PDUType.A_ASSOCIATE_RQ:
168            readPDU();
169            as.onAAssociateRQ((AAssociateRQ) decode(new AAssociateRQ()));
170            return;
171        case PDUType.A_ASSOCIATE_AC:
172            readPDU();
173            as.onAAssociateAC((AAssociateAC) decode(new AAssociateAC()));
174            return;
175        case PDUType.P_DATA_TF:
176            readPDU();
177            as.onPDataTF();
178            return;
179        case PDUType.A_ASSOCIATE_RJ:
180            checkPDULength(4);
181            get();
182            as.onAAssociateRJ(new AAssociateRJ(get(), get(), get()));
183            break;
184        case PDUType.A_RELEASE_RQ:
185            checkPDULength(4);
186            as.onAReleaseRQ();
187            break;
188        case PDUType.A_RELEASE_RP:
189            checkPDULength(4);
190            as.onAReleaseRP();
191            break;
192        case PDUType.A_ABORT:
193            checkPDULength(4);
194            get();
195            get();
196            as.onAAbort(new AAbort(get(), get()));
197            break;
198        default:
199            abort(AAbort.UNRECOGNIZED_PDU, UNRECOGNIZED_PDU);
200        }
201    }
202
203    private void checkThread() {
204        if (th != Thread.currentThread())
205            throw new IllegalStateException("Entered by wrong thread");
206    }
207
208    private void checkPDULength(int len) throws AAbort {
209        if (pdulen != len)
210            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
211    }
212
213    private void readPDU() throws IOException {
214        if (pdulen < 4 || pdulen > MAX_PDU_LEN)
215            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
216
217        if (6 + pdulen > buf.length)
218            buf = Arrays.copyOf(buf, 6 + pdulen);
219
220        readFully(10, pdulen - 4);
221    }
222
223    private void readFully(int off, int len) throws IOException {
224        try {
225            StreamUtils.readFully(in, buf, off, len);
226        } catch (IOException e) {
227            throw e;
228        }
229    }
230
231    private void abort(int reason, String logmsg) throws AAbort {
232        Association.LOG.warn(logmsg,
233                new Object[] { as, pdutype, pdulen & 0xFFFFFFFFL });
234        throw new AAbort(AAbort.UL_SERIVE_PROVIDER, reason);
235    }
236
237    @SuppressWarnings("deprecation")
238    private String getString(int len) {
239        if (pos + len > pdulen + 6)
240            throw new IndexOutOfBoundsException();
241        String s;
242        // Skip illegal trailing NULL
243        int len0 = len;
244        while (len0 > 0 && buf[pos + len0 - 1] == 0) {
245            len0--;
246        }
247        s = new String(buf, 0, pos, len0);
248        pos += len;
249        return s;
250    }
251
252    private String decodeString() {
253        return getString(getUnsignedShort());
254    }
255
256    private AAssociateRQAC decode(AAssociateRQAC rqac)
257            throws AAbort {
258        try {
259            rqac.setProtocolVersion(getUnsignedShort());
260            get();
261            get();
262            rqac.setCalledAET(getString(16).trim());
263            rqac.setCallingAET(getString(16).trim());
264            rqac.setReservedBytes(getBytes(32));
265            while (pos < pdulen)
266                decodeItem(rqac);
267            checkPDULength(pos - 6);
268        } catch (IndexOutOfBoundsException e) {
269            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDU_LENGTH);
270        }
271        return rqac;
272    }
273
274    private void decodeItem(AAssociateRQAC rqac) throws AAbort {
275        int itemType = get();
276        get(); // skip reserved byte
277        int itemLen = getUnsignedShort();
278        switch (itemType)
279        {
280        case ItemType.APP_CONTEXT:
281            rqac.setApplicationContext(getString(itemLen));
282            break;
283        case ItemType.RQ_PRES_CONTEXT:
284        case ItemType.AC_PRES_CONTEXT:
285            rqac.addPresentationContext(decodePC(itemLen));
286            break;
287        case ItemType.USER_INFO:
288            decodeUserInfo(itemLen, rqac);
289            break;
290        default:
291            skip(itemLen);
292        }
293    }
294
295    private PresentationContext decodePC(int itemLen) {
296        int pcid = get();
297        get(); // skip reserved byte
298        int result = get();
299        get(); // skip reserved byte
300        String as = null;
301        ArrayList<String> tss = new ArrayList<String>(1);
302        int endpos = pos + itemLen - 4;
303        while (pos < endpos) {
304            int subItemType = get() & 0xff;
305            get(); // skip reserved byte
306            int subItemLen = getUnsignedShort();
307            switch (subItemType)
308            {
309            case ItemType.ABSTRACT_SYNTAX:
310                as = getString(subItemLen);
311                break;
312            case ItemType.TRANSFER_SYNTAX:
313                tss.add(getString(subItemLen));
314                break;
315            default:
316                skip(subItemLen);
317            }
318        }
319        return new PresentationContext(pcid, result, as,
320                tss.toArray(new String[tss.size()]));
321    }
322
323    private void decodeUserInfo(int itemLength, AAssociateRQAC rqac) throws AAbort
324    {
325        int endpos = pos + itemLength;
326        while (pos < endpos)
327            decodeUserInfoSubItem(rqac);
328    }
329
330    private void decodeUserInfoSubItem(AAssociateRQAC rqac) throws AAbort
331    {
332        int itemType = get();
333        get(); // skip reserved byte
334        int itemLen = getUnsignedShort();
335        switch (itemType)
336        {
337        case ItemType.MAX_PDU_LENGTH:
338            rqac.setMaxPDULength(getInt());
339            break;
340        case ItemType.IMPL_CLASS_UID:
341            rqac.setImplClassUID(getString(itemLen));
342            break;
343        case ItemType.ASYNC_OPS_WINDOW:
344            rqac.setMaxOpsInvoked(getUnsignedShort());
345            rqac.setMaxOpsPerformed(getUnsignedShort());
346            break;
347        case ItemType.ROLE_SELECTION:
348            rqac.addRoleSelection(decodeRoleSelection(itemLen));
349            break;
350        case ItemType.IMPL_VERSION_NAME:
351            rqac.setImplVersionName(getString(itemLen));
352            break;
353        case ItemType.EXT_NEG:
354            rqac.addExtendedNegotiation(decodeExtNeg(itemLen));
355            break;
356        case ItemType.COMMON_EXT_NEG:
357            rqac.addCommonExtendedNegotiation(decodeCommonExtNeg(itemLen));
358            break;
359        case ItemType.RQ_USER_IDENTITY:
360            rqac.setUserIdentityRQ(decodeUserIdentityRQ(itemLen));
361            break;
362        case ItemType.AC_USER_IDENTITY:
363            rqac.setUserIdentityAC(decodeUserIdentityAC(itemLen));
364            break;
365        default:
366            skip(itemLen);
367        }
368    }
369
370    private RoleSelection decodeRoleSelection(int itemLen) {
371        String cuid = decodeString();
372        boolean scu = get() != 0;
373        boolean scp = get() != 0;
374        return new RoleSelection(cuid, scu, scp);
375    }
376
377    private ExtendedNegotiation decodeExtNeg(int itemLen) {
378        int uidLength = getUnsignedShort();
379        String cuid = getString(uidLength);
380        byte[] info = getBytes(itemLen - uidLength - 2);
381        return new ExtendedNegotiation(cuid, info);
382    }
383
384    private CommonExtendedNegotiation decodeCommonExtNeg(int itemLen)
385            throws AAbort {
386        int endPos = pos + itemLen;
387        String sopCUID = getString(getUnsignedShort());
388        String serviceCUID = getString(getUnsignedShort());
389        ArrayList<String> relSopCUIDs = new ArrayList<String>(1);
390        int relSopCUIDsLen = getUnsignedShort();
391        int endRelSopCUIDs  = pos + relSopCUIDsLen;
392        while (pos < endRelSopCUIDs)
393            relSopCUIDs.add(decodeString());
394        if (pos != endRelSopCUIDs || pos > endPos)
395            abort(AAbort.INVALID_PDU_PARAMETER_VALUE,
396                    INVALID_COMMON_EXTENDED_NEGOTIATION);
397        skip(endPos - pos);
398        return new CommonExtendedNegotiation(sopCUID, serviceCUID,
399                relSopCUIDs.toArray(new String[relSopCUIDs.size()]));
400    }
401
402    private UserIdentityRQ decodeUserIdentityRQ(int itemLen) throws AAbort {
403        int endPos = pos + itemLen;
404        int type = get() & 0xff;
405        boolean rspReq = get() != 0;
406        byte[] primaryField = decodeBytes();
407        byte[] secondaryField = decodeBytes();
408        if (pos != endPos)
409            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY);
410        return new UserIdentityRQ(type, rspReq, primaryField, secondaryField);
411    }
412
413    private UserIdentityAC decodeUserIdentityAC(int itemLen) throws AAbort {
414        int endPos = pos + itemLen;
415        byte[] serverResponse = decodeBytes();
416        if (pos != endPos)
417            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_USER_IDENTITY);
418        return new UserIdentityAC(serverResponse);
419    }
420
421    public void decodeDIMSE() throws IOException {
422        checkThread();
423        if (pcid != - 1)
424            return; // already inside decodeDIMSE
425
426        nextPDV(PDVType.COMMAND, -1);
427
428        PresentationContext pc = as.getPresentationContext(pcid);
429        if (pc == null) {
430            Association.LOG.warn(
431                    "{}: No Presentation Context with given ID - {}",
432                    as, pcid);
433            throw new AAbort();
434        }
435
436        if (!pc.isAccepted()) {
437            Association.LOG.warn(
438                    "{}: No accepted Presentation Context with given ID - {}",
439                    as, pcid);
440            throw new AAbort();
441        }
442
443        Attributes cmd = readCommand();
444        Dimse dimse = dimseOf(cmd);
445        String tsuid = pc.getTransferSyntax();
446        if (Dimse.LOG.isInfoEnabled()) {
447            Dimse.LOG.info("{} >> {}", as, dimse.toString(cmd, pcid, tsuid));
448            Dimse.LOG.debug("Command:\n{}", cmd);
449        }
450        if (dimse == Dimse.C_CANCEL_RQ) {
451            as.onCancelRQ(cmd);
452        } else if (Commands.hasDataset(cmd)) {
453            nextPDV(PDVType.DATA, pcid);
454            if (dimse.isRSP()) {
455                Attributes data = readDataset(tsuid);
456                Dimse.LOG.debug("Dataset:\n{}", data);
457                as.onDimseRSP(dimse, cmd, data);
458            } else {
459                as.onDimseRQ(pc, dimse, cmd, this);
460                long skipped = skipAll();
461                if (skipped > 0)
462                    Association.LOG.debug(
463                        "{}: Service User did not consume {} bytes of DIMSE data.",
464                        as, skipped);
465            }
466            skipAll();
467        } else {
468            if (dimse.isRSP()) {
469                as.onDimseRSP(dimse, cmd, null);
470            } else {
471                as.onDimseRQ(pc, dimse, cmd, null);
472            }
473        }
474        pcid = -1;
475    }
476
477    private Dimse dimseOf(Attributes cmd) throws AAbort {
478        try {
479            return Dimse.valueOf(cmd.getInt(Tag.CommandField, 0));
480        } catch (IllegalArgumentException e) {
481            Dimse.LOG.info("{}: illegal DIMSE:", as);
482            Dimse.LOG.info("\n{}", cmd);
483            throw new AAbort();
484        }
485    }
486
487    private Attributes readCommand() throws IOException {
488        DicomInputStream in =
489                new DicomInputStream(this, UID.ImplicitVRLittleEndian);
490        try {
491            return in.readCommand();
492        } finally {
493            SafeClose.close(in);
494        }
495    }
496
497    @Override
498    public Attributes readDataset(String tsuid) throws IOException {
499        DicomInputStream in = new DicomInputStream(this, tsuid);
500        try {
501            return in.readDataset(-1, -1);
502        } finally {
503            SafeClose.close(in);
504        }
505    }
506
507    private void nextPDV(int expectedPDVType, int expectedPCID)
508            throws IOException {
509        if (!hasRemaining()) {
510            nextPDU();
511            if (pdutype != PDUType.P_DATA_TF) {
512                Association.LOG.info(
513                        "{}: Expected P-DATA-TF PDU but received PDU[type={}]",
514                        as, pdutype);
515                throw new EOFException();
516            }
517        }
518        if (remaining() < 6)
519            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV);
520        int pdvlen = getInt();
521        this.pdvend = pos + pdvlen;
522        if (pdvlen < 2 || pdvlen > remaining())
523            abort(AAbort.INVALID_PDU_PARAMETER_VALUE, INVALID_PDV);
524        this.pcid = get();
525        this.pdvmch = get();
526        Association.LOG.trace("{} >> PDV[len={}, pcid={}, mch={}]",
527                new Object[] { as, pdvlen, pcid, pdvmch } );
528        if ((pdvmch & PDVType.COMMAND) != expectedPDVType)
529            abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_TYPE);
530        if (expectedPCID != -1 && pcid != expectedPCID)
531            abort(AAbort.UNEXPECTED_PDU_PARAMETER, UNEXPECTED_PDV_PCID);
532    }
533
534    private boolean isLastPDV() throws IOException {
535        while (pos == pdvend) {
536            if ((pdvmch & PDVType.LAST) != 0)
537                return true;
538            nextPDV(pdvmch & PDVType.COMMAND, pcid);
539        }
540        return false;
541    }
542
543    @Override
544    public int read() throws IOException {
545        if (th != Thread.currentThread())
546            throw new IllegalStateException("Entered by wrong thread");
547        if (isLastPDV())
548            return -1;
549
550        return get();
551    }
552
553    @Override
554    public int read(byte[] b, int off, int len) throws IOException {
555        if (th != Thread.currentThread())
556            throw new IllegalStateException("Entered by wrong thread");
557        if (isLastPDV())
558            return -1;
559
560        int read = Math.min(len, pdvend - pos);
561        get(b, off, read);
562        return read;
563    }
564
565    @Override
566    public final int available() {
567        return pdvend - pos;
568    }
569
570    @Override
571    public long skip(long n) throws IOException
572    {
573        if (th != Thread.currentThread())
574            throw new IllegalStateException("Entered by wrong thread");
575        if (n <= 0 || isLastPDV())
576            return 0;
577
578        int skipped = (int) Math.min(n, pdvend - pos);
579        skip(skipped);
580        return skipped;
581    }
582    
583    @Override
584    public void close() throws IOException {
585        if (th != Thread.currentThread())
586            throw new IllegalStateException("Entered by wrong thread");
587        skipAll();
588    }
589
590    @Override
591    public long skipAll() throws IOException {
592        if (th != Thread.currentThread())
593            throw new IllegalStateException("Entered by wrong thread");
594        long n = 0;
595        while (!isLastPDV()) {
596            n += pdvend - pos;
597            pos = pdvend;
598        }
599        return n;
600    }
601
602    @Override
603    public void copyTo(OutputStream out, int length) throws IOException {
604        if (th != Thread.currentThread())
605            throw new IllegalStateException("Entered by wrong thread");
606        int remaining = length;
607        while (remaining > 0) {
608            if (isLastPDV())
609                throw new EOFException("remaining: " + remaining);
610            int read = Math.min(remaining, pdvend - pos);
611            out.write(buf, pos, read);
612            remaining -= read;
613            pos += read;
614        }
615    }
616
617    @Override
618    public void copyTo(OutputStream out) throws IOException {
619        if (th != Thread.currentThread())
620            throw new IllegalStateException("Entered by wrong thread");
621        while (!isLastPDV()) {
622            out.write(buf, pos, pdvend - pos);
623            pos = pdvend;
624        }
625    }
626}