Repository: chrovis/cljam
Pull request #173 (2019-07-30 08:30:59)

Fix decoding of long CIGAR ops in BAM
#### Problem
`cljam.io.bam` fails to decode alignments with CIGAR operations longer than 134217728

#### Cause
A CIGAR is stored as an array of `op_len<<4|op` in `uint32_t[n_cigar_op]`.
Thus `op_len` can be as long as 28bit `0x0FFFFFFF` but it is not handled properly due to type promotion from int to long.

#### Changes
Use `Integer/toUnsignedLong` to get a positive long value.

#### Tests

- `lein check` 🆗
- `lein test :all` 🆗
- `lein cloverage` 🆗
- `lein eastwood` 🆗


Patch:
diff --git a/src/cljam/io/sam/util/cigar.clj b/src/cljam/io/sam/util/cigar.clj
index fbd7cb2..05f57f0 100644
--- a/src/cljam/io/sam/util/cigar.clj
+++ b/src/cljam/io/sam/util/cigar.clj
@@ -69,9 +69,9 @@
     (.order buf ByteOrder/LITTLE_ENDIAN)
     (loop [ref-length 0]
       (if (.hasRemaining buf)
-        (let [b (.getInt buf)
+        (let [b (Integer/toUnsignedLong (.getInt buf))
               op (bit-and b 0xF)
-              n (bit-shift-right b 4)]
+              n (unsigned-bit-shift-right b 4)]
           (recur (+ ref-length (case op 0 n 2 n 3 n 7 n 8 n 0))))
         ref-length))))
 
@@ -84,9 +84,9 @@
     (.order buf ByteOrder/LITTLE_ENDIAN)
     (loop [ref-length 0]
       (if (.hasRemaining buf)
-        (let [b (.getInt buf)
+        (let [b (Integer/toUnsignedLong (.getInt buf))
               op (bit-and b 0xF)
-              n  (bit-shift-right b 4)]
+              n  (unsigned-bit-shift-right b 4)]
           (doto sb
             (.append n)
             (.append (case op 0 \M 1 \I 2 \D 3 \N 4 \S 5 \H 6 \P 7 \= 8 \X)))


Test patch:
diff --git a/test/cljam/io/sam/util/cigar_test.clj b/test/cljam/io/sam/util/cigar_test.clj
index d060a59..15ebfef 100644
--- a/test/cljam/io/sam/util/cigar_test.clj
+++ b/test/cljam/io/sam/util/cigar_test.clj
@@ -1,6 +1,47 @@
 (ns cljam.io.sam.util.cigar-test
   (:require [clojure.test :refer :all]
-            [cljam.io.sam.util.cigar :as cigar]))
+            [cljam.io.sam.util.cigar :as cigar])
+  (:import [java.nio ByteBuffer ByteOrder]))
+
+(defn- ints->bytes ^bytes [ints]
+  (let [b (ByteBuffer/allocate (* 4 (count ints)))
+        _ (.order b ByteOrder/LITTLE_ENDIAN)
+        ib (.asIntBuffer b)]
+    (doseq [i ints]
+      (.put ib (unchecked-int i)))
+    (.array b)))
+
+(deftest count-ref-bytes
+  (are [?cigar ?expected]
+       (= ?expected
+          (-> ?cigar
+              cigar/encode-cigar
+              ints->bytes
+              cigar/count-ref-bytes))
+    "10M" 10
+    "10S" 0
+    "10M10I" 10
+    "10M10D" 20
+    "134217727M" 134217727 ;; 2^27 - 1, 0x07FFFFFF
+    "134217728M" 134217728 ;; 2^27    , 0x08000000
+    "268435455M" 268435455 ;; 2^28 - 1, 0x0FFFFFFF
+    ))
+
+(deftest decode-cigar-and-ref-length
+  (are [?cigar ?ref-length]
+       (= [?cigar ?ref-length]
+          (-> ?cigar
+              cigar/encode-cigar
+              ints->bytes
+              cigar/decode-cigar-and-ref-length))
+    "10M" 10
+    "10S" 0
+    "10M10I" 10
+    "10M10D" 20
+    "134217727M" 134217727 ;; 2^27 - 1, 0x07FFFFFF
+    "134217728M" 134217728 ;; 2^27    , 0x08000000
+    "268435455M" 268435455 ;; 2^28 - 1, 0x0FFFFFFF
+    ))
 
 (deftest about-parse
   (is (= (cigar/parse  "1S2I6M1P11I") '([1 \S] [2 \I] [6 \M] [1 \P] [11 \I]))))