rstango 0.3.0

Implementation of the Tango distributed control system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
//! Marshalling and unmarshalling of types from the Tango IDL.
//! 
//! See file:///usr/include/tango.idl, or any path you installed it.

use std::time::{Duration, SystemTime};
use std::borrow::Cow;
use std::fmt::Debug;
use std::mem;

use crate::value::{AttrDataFormat, AttrDataKind, CommandDataKind, DevEncoded, DevEnum, TangoElement, Value, ValueFormat};
use crate::error::{reason, Error, ErrorKind, ErrorSeverity, Result};
use crate::value::{Scalar, Spectrum};
use crate::buf::Buf;
use crate::dev::{
    AttrArchiveEventConfig, AttrChangeEventConfig, AttrConfig, AttrDeltaConfig, AttrInfo,
    AttrMemorized, AttrMode, AttrNumber, AttrPeriodicEventConfig, AttrQuality,
    AttrRangeConfig, AttrRead, CommandInfo, DevInfo, DevSource, DevState, DisplayLevel, Named, NamedResult};

use super::orb::cdr::{CdrReader, CdrWriter, CdrRead, CdrWrite};
use super::DevVersion;


// ================= //
//   CORBA Support   //
// ================= //

/// Internal macro to generate a Tango repository ID.
macro_rules! tango_repo_id {
    ($ty:literal) => { concat!("IDL:Tango/", $ty, ":1.0") };
}

pub(crate) use tango_repo_id as tango_repo_id;
use uuid::Uuid;

// The typecode kinds constants used by Tango for the Any values transfered for commands
// and attributes!
const TK_NULL: u32 = 0;
const TK_VOID: u32 = 1;
const TK_SHORT: u32 = 2;
const TK_LONG: u32 = 3;
const TK_USHORT: u32 = 4;
const TK_ULONG: u32 = 5;
const TK_FLOAT: u32 = 6;
const TK_DOUBLE: u32 = 7;
const TK_BOOL: u32 = 8;
const TK_OCTET: u32 = 10;
const TK_STRUCT: u32 = 15;
const TK_ENUM: u32 = 17;
const TK_STRING: u32 = 18;
const TK_SEQUENCE: u32 = 19;
const TK_ALIAS: u32 = 21;
const TK_LONGLONG: u32 = 23;
const TK_ULONGLONG: u32 = 24;

// The 'AttributeDataType' discriminants of the attribute value union used from the IDL v4
// onward, in place of the Any the earlier versions carry.
const ATT_BOOL: u32 = 0;
const ATT_SHORT: u32 = 1;
const ATT_LONG: u32 = 2;
const ATT_LONG64: u32 = 3;
const ATT_FLOAT: u32 = 4;
const ATT_DOUBLE: u32 = 5;
const ATT_UCHAR: u32 = 6;
const ATT_USHORT: u32 = 7;
const ATT_ULONG: u32 = 8;
const ATT_ULONG64: u32 = 9;
const ATT_STRING: u32 = 10;
const ATT_STATE: u32 = 11;
const ATT_DEVICE_STATE: u32 = 12;
const ATT_ENCODED: u32 = 13;
const ATT_NO_DATA: u32 = 14;

// The 'AttrDataFormat' enumerators of the IDL, FMT_UNKNOWN being represented by the absence
// of a format.
const FMT_SCALAR: u32 = 0;
const FMT_SPECTRUM: u32 = 1;
const FMT_IMAGE: u32 = 2;
const FMT_UNKNOWN: u32 = 3;

// The 'CmdArgType' enumerators, which the IDL does not declare but that the data_type
// field of an attribute value from v5 onward holds, as well as the input and output types
// of a command and the type of an attribute in its configuration. For an attribute this is
// always the scalar element type whatever the format of the value is, the array
// enumerators only ever being the type of a command.
//
// Note that this is the *declared* type of the attribute and not the one of the union
// arm carrying it, which are only the same as long as no DevEnum is involved: those
// travel as shorts, and data_type is the only thing telling them from a real DevShort.
// We have no enumerated variant in the data module, so the two always match for us.
//
// See 'CmdArgType' in tango_const.h, whose 27 is deliberately left out, having been an
// unused enumerator that was removed while keeping the following ones where they were.
const DEV_VOID: i32 = 0;
const DEV_BOOLEAN: i32 = 1;
const DEV_SHORT: i32 = 2;
const DEV_LONG: i32 = 3;
const DEV_FLOAT: i32 = 4;
const DEV_DOUBLE: i32 = 5;
const DEV_USHORT: i32 = 6;
const DEV_ULONG: i32 = 7;
const DEV_STRING: i32 = 8;
const DEVVAR_CHARARRAY: i32 = 9;
const DEVVAR_SHORTARRAY: i32 = 10;
const DEVVAR_LONGARRAY: i32 = 11;
const DEVVAR_FLOATARRAY: i32 = 12;
const DEVVAR_DOUBLEARRAY: i32 = 13;
const DEVVAR_USHORTARRAY: i32 = 14;
const DEVVAR_ULONGARRAY: i32 = 15;
const DEVVAR_STRINGARRAY: i32 = 16;
const DEVVAR_LONGSTRINGARRAY: i32 = 17;
const DEVVAR_DOUBLESTRINGARRAY: i32 = 18;
const DEV_STATE: i32 = 19;
const CONST_DEV_STRING: i32 = 20;
const DEVVAR_BOOLEANARRAY: i32 = 21;
const DEV_UCHAR: i32 = 22;
const DEV_LONG64: i32 = 23;
const DEV_ULONG64: i32 = 24;
const DEVVAR_LONG64ARRAY: i32 = 25;
const DEVVAR_ULONG64ARRAY: i32 = 26;
const DEV_ENCODED: i32 = 28;
const DEV_ENUM: i32 = 29;
const DEV_PIPE_BLOB: i32 = 30;
const DEVVAR_STATEARRAY: i32 = 31;
const DEVVAR_ENCODEDARRAY: i32 = 32;
const DATA_TYPE_UNKNOWN: i32 = 100;

// The 'DispLevel' enumerators, carried by the command and attribute configurations.
const DL_OPERATOR: u32 = 0;
const DL_EXPERT: u32 = 1;
const DL_UNKNOWN: u32 = 2;

// The 'AttrWriteType' enumerators, carried by the attribute configuration.
const WT_READ: u32 = 0;
const WT_READ_WITH_WRITE: u32 = 1;
const WT_WRITE: u32 = 2;
const WT_READ_WRITE: u32 = 3;
const WT_UNKNOWN: u32 = 4;

// The strings cppTango sends for a configuration property a device has no value for, which
// we give back as no value at all, an empty string doing the same. See tango_const.h, where
// they are respectively AlrmValueNotSpec, NotANumber, LabelNotSpec, DescNotSpec,
// StdUnitNotSpec, DispUnitNotSpec and NotSet.
//
// Note that the display format has no placeholder of its own, FormatNotSpec being "%6.2f",
// a real format that a device means, so only NOT_SPECIFIED stands for an unset one.
const NOT_SPECIFIED: &str = "Not specified";
const NOT_A_NUMBER: &str = "NaN";
const NO_LABEL: &str = "No label";
const NO_DESCRIPTION: &str = "No description";
const NO_STANDARD_UNIT: &str = "No standard unit";
const NO_DISPLAY_UNIT: &str = "No display unit";
const UNINITIALIZED: &str = "Uninitialised";

// ================= //
//  Simple adapters  //
// ================= //

/// Internal macro to generate a CDR adapter for an enumeration.
macro_rules! def_cdr_enum_adapter {
    ( $enum:ty { $( $variant:ident = $value:literal, )* } ) => {

        impl CdrRead<'_> for $enum {
            fn read(buf: &mut CdrReader<'_>) -> Result<Self> {
                Ok(match buf.read_ulong()? {
                    $( $value => Self::$variant,)*
                    id => return Err(Error::new(ErrorKind::InvalidData)
                        .with_description(format!(concat!("Invalid ", stringify!($enum), " numeric value: {}"), id))),
                })
            }
        }

        impl CdrWrite for $enum {
            fn write(&self, buf: &mut CdrWriter) -> Result<()> {
                buf.write_ulong(match self {
                    $( Self::$variant => $value,)*
                })
            }
        }

    };
}

def_cdr_enum_adapter!(ErrorSeverity {
    Warning = 0,
    Error = 1,
    Panic = 2,
});

def_cdr_enum_adapter!(DevState {
    On = 0,
    Off = 1,
    Close = 2,
    Open = 3,
    Insert = 4,
    Extract = 5,
    Moving = 6,
    Standby = 7,
    Fault = 8,
    Init = 9,
    Running = 10,
    Alarm = 11,
    Disable = 12,
    Unknown = 13,
});

def_cdr_enum_adapter!(DevSource {
    Dev = 0,
    Cache = 1,
    CacheDev = 2,
});

def_cdr_enum_adapter!(AttrQuality {
    Valid = 0,
    Invalid = 1,
    Alarm = 2,
    Changing = 3,
    Warning = 4,
});

impl CdrRead<'_> for Option<Error> {
    fn read(buf: &mut CdrReader<'_>) -> Result<Self> {
        
        let mut top_err = None;

        // A dev failed contains only one sequence of DevError structures...
        let len = buf.read_sequence_len()?;
        for _ in 0..len {

            let reason = buf.read_string()?;
            let mut err = Error::new(ErrorKind::Reason(&reason));
            err.set_severity(buf.read()?);
            err.set_description(buf.read_string()?.into_owned());
            err.set_origin(buf.read_string()?.into_owned());

            if let Some(parent_err) = top_err.take() {
                err.set_parent(parent_err);
            }

            top_err = Some(err);

        }

        Ok(top_err)

    }
}

impl CdrWrite for Option<Error> {
    #[inline]
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {
        buf.write(&self.as_ref())
    }
}

impl CdrWrite for Option<&Error> {
    #[inline]
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {
        if let Some(error) = self {
            buf.write(error)
        } else {
            buf.write_sequence_len(0)
        }
    }
}

impl CdrWrite for Error {
    #[inline]
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {
        
        // The chain is walked from the error down to its deepest parent, which is the order
        // the reading above builds it back from, so it goes out reversed.
        let mut chain = Vec::new();
        let mut next = Some(self);
        while let Some(err) = next {
            chain.push(err);
            next = err.parent();
        }

        buf.write_sequence_len(chain.len())?;
        for err in chain.iter().rev() {
            buf.write_string(err.reason())?;
            buf.write(&err.severity())?;
            buf.write_string(err.description())?;
            buf.write_string(err.origin())?;
        }

        Ok(())

    }
}

impl CdrRead<'_> for Error {
    fn read(buf: &mut CdrReader<'_>) -> Result<Self> {
        let err = buf.read::<Option<Error>>()?;
        err.ok_or_else(|| Error::new(ErrorKind::InvalidData)
            .with_description("The given DevFailed should have at least one error"))
    }
}

impl CdrRead<'_> for SystemTime {

    /// Read a `TimeVal`, which is three longs and nothing else, so nothing about one says
    /// it holds a time: a sender with no time to give sends whatever it has.
    ///
    /// cppTango is such a sender. `TimeVal` is generated without a constructor and
    /// `DeviceAttribute::time` declared without an initialiser, so the timestamp of a
    /// write request is whatever was on the client's stack — seen here as the halves of a
    /// pointer, the seconds and the nanoseconds 28 apart and the microseconds a steady
    /// `0x7Fxx`, and negative about a third of the time.
    ///
    /// Which is why a component that cannot be part of a time is taken as no time at all
    /// rather than as a reason to fail. Failing here would refuse a whole request over a
    /// field nothing reads: a server drops the timestamp of a write, see
    /// [`super::borrow_attr_values`], and a reply for an attribute that failed carries no
    /// time either. Neither can the arithmetic overflow any more — it could, which a debug
    /// build turned into a panic and a release build into a time that was quietly wrong.
    fn read(buf: &mut CdrReader<'_>) -> Result<Self> {

        let tv_sec = buf.read_long()?;
        let tv_usec = buf.read_long()?;
        let tv_nsec = buf.read_long()?;

        // Each of the three is a count of something, so a negative one is not a time that
        // is merely wrong: it is a field that was never filled in.
        let (Ok(secs), Ok(usecs), Ok(nsecs)) = (
            u64::try_from(tv_sec),
            u64::try_from(tv_usec),
            u64::try_from(tv_nsec),
        ) else {
            return Ok(SystemTime::UNIX_EPOCH);
        };

        let since_epoch = Duration::from_secs(secs)
            .checked_add(Duration::from_micros(usecs))
            .and_then(|elapsed| elapsed.checked_add(Duration::from_nanos(nsecs)));

        Ok(since_epoch
            .and_then(|since_epoch| SystemTime::UNIX_EPOCH.checked_add(since_epoch))
            .unwrap_or(SystemTime::UNIX_EPOCH))

    }

}

impl CdrWrite for SystemTime {
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {

        let since_epoch = self.duration_since(SystemTime::UNIX_EPOCH)
            .map_err(|_| Error::new(ErrorKind::InvalidData)
                .with_description("Cannot write a system time that is before the UNIX epoch"))?;

        // The three components are longs in the IDL, so the seconds are bounded like any
        // 32 bits UNIX timestamp is.
        let tv_sec = i32::try_from(since_epoch.as_secs())
            .map_err(|_| Error::new(ErrorKind::InvalidData)
                .with_description("Cannot write a system time whose seconds do not fit into a long"))?;

        // Splitting the sub-second part the way the reading above puts it back together.
        let subsec_nanos = since_epoch.subsec_nanos();

        buf.write_long(tv_sec)?;
        buf.write_long((subsec_nanos / 1_000) as i32)?;
        buf.write_long((subsec_nanos % 1_000) as i32)

    }
}

/// Equivalent to `ClntIdent` in the IDL.
/// 
/// TODO: For now we don't support IDL v6 OpenTelemetry trace context!
#[derive(Debug,)]
pub enum ClientIdent<'a> {
    Cpp {
        id: u32,
    },
    Java {
        main_class: Cow<'a, str>,
        uuid: Uuid,
    },
}

impl CdrWrite for ClientIdent<'_> {
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {
        match self {
            ClientIdent::Cpp { id } => {
                buf.write_ulong(0)?;
                buf.write_ulong(*id)?;
            }
            ClientIdent::Java { main_class, uuid } => {
                buf.write_ulong(1)?;
                buf.write_string(main_class)?;
                // 'JavaUUID' is 'unsigned long long[2]', an array and not a sequence, so
                // it has no length, but it is 8-aligned like any long long. The pair is
                // most-significant first, matching Java's UUID accessors.
                let (msb, lsb) = uuid.as_u64_pair();
                buf.write_ulonglong_slice(&[msb, lsb])?;
            }
        }
        Ok(())
    }
}

// ================= //
//   Element types   //
// ================= //

/// How the values of a Tango element type travel, and how they are announced: the typecode
/// of a scalar of them in an `any`, the repository ID of the alias of a sequence of them,
/// the discriminant of their arm of the attribute value union, and their declared type.
trait CdrTangoElement: TangoElement {
    const TK: u32;
    const ARRAY_REPO_ID: &'static str;
    const ATT: u32;
    const DEV_TYPE: i32;
    /// Read `len` values, without the length that precedes a sequence.
    fn read_values(buf: &mut CdrReader<'_>, len: usize) -> Result<Spectrum<Self>>;
    /// Write the values of that data, in column-major order and without the length that
    /// precedes a sequence.
    fn write_values<const N: usize>(buf: &mut CdrWriter, data: &Buf<Self, N>) -> Result<()>;
}

/// The CDR primitives, whose arrays are shared out of the message, see
/// [`CdrReader::read_buf`].
macro_rules! impl_cdr_tango_element {
    ( $( $ty:ty => $tk:ident, $array:literal, $att:ident, $dev:ident; )* ) => {
        $(
            impl CdrTangoElement for $ty {
                const TK: u32 = $tk;
                const ARRAY_REPO_ID: &'static str = tango_repo_id!($array);
                const ATT: u32 = $att;
                const DEV_TYPE: i32 = $dev;
                fn read_values(buf: &mut CdrReader<'_>, len: usize) -> Result<Spectrum<Self>> {
                    buf.read_buf(len)
                }
                fn write_values<const N: usize>(buf: &mut CdrWriter, data: &Buf<Self, N>) -> Result<()> {
                    buf.write_data(data)
                }
            }
        )*
    };
}

impl_cdr_tango_element! {
    bool => TK_BOOL, "DevVarBooleanArray", ATT_BOOL, DEV_BOOLEAN;
    u8 => TK_OCTET, "DevVarCharArray", ATT_UCHAR, DEV_UCHAR;
    u16 => TK_USHORT, "DevVarUShortArray", ATT_USHORT, DEV_USHORT;
    i16 => TK_SHORT, "DevVarShortArray", ATT_SHORT, DEV_SHORT;
    u32 => TK_ULONG, "DevVarULongArray", ATT_ULONG, DEV_ULONG;
    i32 => TK_LONG, "DevVarLongArray", ATT_LONG, DEV_LONG;
    u64 => TK_ULONGLONG, "DevVarULong64Array", ATT_ULONG64, DEV_ULONG64;
    i64 => TK_LONGLONG, "DevVarLong64Array", ATT_LONG64, DEV_LONG64;
    f32 => TK_FLOAT, "DevVarFloatArray", ATT_FLOAT, DEV_FLOAT;
    f64 => TK_DOUBLE, "DevVarDoubleArray", ATT_DOUBLE, DEV_DOUBLE;
    // An enumeration travels as the short it is, only its declared type telling it apart.
    DevEnum => TK_SHORT, "DevVarShortArray", ATT_SHORT, DEV_ENUM;
}

/// A state is an enum, which is an ulong on the wire and one octet in memory, so its values
/// are always copied.
impl CdrTangoElement for DevState {
    const TK: u32 = TK_ENUM;
    const ARRAY_REPO_ID: &'static str = tango_repo_id!("DevVarStateArray");
    const ATT: u32 = ATT_STATE;
    const DEV_TYPE: i32 = DEV_STATE;
    fn read_values(buf: &mut CdrReader<'_>, len: usize) -> Result<Spectrum<Self>> {
        buf.read_vec(len).map(Buf::from)
    }
    fn write_values<const N: usize>(buf: &mut CdrWriter, data: &Buf<Self, N>) -> Result<()> {
        data.iter().try_for_each(|state| buf.write(&state))
    }
}

impl CdrTangoElement for str {
    const TK: u32 = TK_STRING;
    const ARRAY_REPO_ID: &'static str = tango_repo_id!("DevVarStringArray");
    const ATT: u32 = ATT_STRING;
    const DEV_TYPE: i32 = DEV_STRING;
    fn read_values(buf: &mut CdrReader<'_>, len: usize) -> Result<Spectrum<Self>> {
        buf.read_string_buf(len)
    }
    fn write_values<const N: usize>(buf: &mut CdrWriter, data: &Buf<Self, N>) -> Result<()> {
        buf.write_string_data(data)
    }
}

/// Read a sequence, its length included.
fn read_seq<T: CdrTangoElement + ?Sized>(buf: &mut CdrReader<'_>) -> Result<Spectrum<T>> {
    let len = buf.read_sequence_len()?;
    T::read_values(buf, len)
}

/// Write a sequence, its length included.
fn write_seq<T: CdrTangoElement + ?Sized, const N: usize>(buf: &mut CdrWriter, data: &Buf<T, N>) -> Result<()> {
    buf.write_sequence_len(data.len())?;
    T::write_values(buf, data)
}

/// Read a single value, as the scalar it is.
fn read_scalar<T: CdrTangoElement + ?Sized>(buf: &mut CdrReader<'_>) -> Result<Value> {
    let value = T::read_values(buf, 1)?.into_scalar().expect("a single value was read");
    Ok(T::wrap(ValueFormat::Scalar(value)))
}

/// Read a sequence, as the spectrum it is.
fn read_spectrum<T: CdrTangoElement + ?Sized>(buf: &mut CdrReader<'_>) -> Result<Value> {
    Ok(T::wrap(ValueFormat::Spectrum(read_seq(buf)?)))
}

fn write_scalar_tc(buf: &mut CdrWriter, tk: u32) -> Result<()> {

    buf.write_ulong(tk)?;

    if tk == TK_STRING {
        buf.write_ulong(0)?;  // unbounded string!
    } else if tk == TK_ENUM {
        buf.write_encapsulation(|param| {
            param.write_string(tango_repo_id!("DevState"))?;
            param.write_string("DevState")?;
            param.write(&["ON", "OFF", "CLOSE", "OPEN", "INSERT", "EXTRACT", "MOVING", "STANDBY", "FAULT", "INIT", "RUNNING", "ALARM", "DISABLE", "UNKNOWN"])
        })?;
    }

    Ok(())

}

fn write_array_tc(buf: &mut CdrWriter, tk: u32, repo_id: &str) -> Result<()> {

    let name = &repo_id[10..repo_id.len() - 4];

    buf.write_ulong(TK_ALIAS)?;

    buf.write_encapsulation(|alias| {
        alias.write_string(repo_id)?;
        alias.write_string(name)?;
        alias.write_ulong(TK_SEQUENCE)?;
        alias.write_encapsulation(|sequence| {
            write_scalar_tc(sequence, tk)?;
            sequence.write_ulong(0)  // unbounded sequence!
        })
    })

}

fn write_any_string_array_tc(buf: &mut CdrWriter, repo_id: &str, first_name: &str, first_tk: u32, first_repo_id: &str) -> Result<()> {

    let name = &repo_id[10..repo_id.len() - 4];

    buf.write_ulong(TK_STRUCT)?;

    buf.write_encapsulation(|param| {

        param.write_string(repo_id)?;
        param.write_string(name)?;

        // The parameters of a tk_struct carry the number of members before the members
        // themselves, and a server refuses the whole request with a marshalling error when
        // it is missing. Note that the reading side skips this, only ever taking the
        // repository ID out of the parameters.
        param.write_ulong(2)?;

        param.write_string(first_name)?;
        write_array_tc(param, first_tk, first_repo_id)?;
        param.write_string("svalue")?;
        write_array_tc(param, TK_STRING, tango_repo_id!("DevVarStringArray"))

    })

}

/// Write the TypeCode of a `DevEncoded`, whose format is a `DevString` and not a bare
/// string, the IDL declaring it through that alias.
fn write_encoded_tc(buf: &mut CdrWriter) -> Result<()> {

    buf.write_ulong(TK_STRUCT)?;

    buf.write_encapsulation(|param| {
        param.write_string(tango_repo_id!("DevEncoded"))?;
        param.write_string("DevEncoded")?;
        param.write_ulong(2)?;
        param.write_string("encoded_format")?;
        param.write_ulong(TK_ALIAS)?;
        param.write_encapsulation(|alias| {
            alias.write_string(tango_repo_id!("DevString"))?;
            alias.write_string("DevString")?;
            write_scalar_tc(alias, TK_STRING)
        })?;
        param.write_string("encoded_data")?;
        write_array_tc(param, u8::TK, u8::ARRAY_REPO_ID)
    })

}

/// Read a `DevEncoded`, its format and its octets sharing the message.
fn read_encoded(buf: &mut CdrReader<'_>) -> Result<DevEncoded> {
    Ok(DevEncoded { format: buf.read::<Scalar<str>>()?, data: read_seq(buf)? })
}

/// Write a `DevEncoded`.
fn write_encoded(buf: &mut CdrWriter, encoded: &DevEncoded) -> Result<()> {
    buf.write_string(encoded.format.value())?;
    write_seq(buf, &encoded.data)
}

// ================= //
//      Command      //
// ================= //

/// Write the `any` a command travels in.
pub fn write_command_data(buf: &mut CdrWriter, value: &Value) -> Result<()> {

    fn format<T: CdrTangoElement + ?Sized>(buf: &mut CdrWriter, format: &ValueFormat<T>) -> Result<()> {
        match format {
            ValueFormat::Scalar(value) => {
                write_scalar_tc(buf, T::TK)?;
                T::write_values(buf, value)
            }
            ValueFormat::Spectrum(values) => {
                write_array_tc(buf, T::TK, T::ARRAY_REPO_ID)?;
                write_seq(buf, values)
            }
            ValueFormat::Image(_) => Err(Error::new(ErrorKind::InvalidData)
                .with_description("Image data cannot be used as command parameter")),
        }
    }

    fn long_double_string<T: CdrTangoElement>(buf: &mut CdrWriter, repo_id: &str, name: &str, values: &Spectrum<T>, strings: &Spectrum<str>) -> Result<()> {
        write_any_string_array_tc(buf, repo_id, name, T::TK, T::ARRAY_REPO_ID)?;
        write_seq(buf, values)?;
        write_seq(buf, strings)
    }

    // Note that enumerations are actually not supported for commands, but we just send the
    // underlying representation anyway.
    match value {
        Value::Bool(values) => format(buf, values),
        Value::Octet(values) => format(buf, values),
        Value::UShort(values) => format(buf, values),
        Value::Short(values) => format(buf, values),
        Value::ULong(values) => format(buf, values),
        Value::Long(values) => format(buf, values),
        Value::ULong64(values) => format(buf, values),
        Value::Long64(values) => format(buf, values),
        Value::Float(values) => format(buf, values),
        Value::Double(values) => format(buf, values),
        Value::Enum(values) => format(buf, values),
        Value::State(values) => format(buf, values),
        Value::String(values) => format(buf, values),
        Value::Void => write_scalar_tc(buf, TK_NULL),
        Value::LongString(longs, strings) => 
            long_double_string(buf, tango_repo_id!("DevVarLongStringArray"), "lvalue", longs, strings),
        Value::DoubleString(doubles, strings) => 
            long_double_string(buf, tango_repo_id!("DevVarDoubleStringArray"), "dvalue", doubles, strings),
        Value::Encoded(encoded) => {
            write_encoded_tc(buf)?;
            write_encoded(buf, encoded)
        }
    }

}

/// Read the `any` a command travels in, out of the octets it was received in: its arrays
/// and strings share them, see [`CdrReader::read_buf`].
///
/// The reader is left right past the value, so that the arguments a later version of the
/// operation carries after it can be read out of it.
pub fn read_command_data(buf: &mut CdrReader<'_>) -> Result<Value> {

    Ok(match buf.read_ulong()? {
        TK_NULL => Value::Void,
        TK_BOOL => read_scalar::<bool>(buf)?,
        TK_OCTET => read_scalar::<u8>(buf)?,
        TK_SHORT => read_scalar::<i16>(buf)?,
        TK_USHORT => read_scalar::<u16>(buf)?,
        TK_LONG => read_scalar::<i32>(buf)?,
        TK_ULONG => read_scalar::<u32>(buf)?,
        TK_LONGLONG => read_scalar::<i64>(buf)?,
        TK_ULONGLONG => read_scalar::<u64>(buf)?,
        TK_FLOAT => read_scalar::<f32>(buf)?,
        TK_DOUBLE => read_scalar::<f64>(buf)?,
        TK_STRING => {
            let _max_len = buf.read_ulong()?;
            read_scalar::<str>(buf)?
        }
        TK_ENUM => {
            // tk_enum, whose parameters are an encapsulation read in place.
            let repo_id = buf.read_encapsulation()?.read_string()?;
            if repo_id != tango_repo_id!("DevState") {
                return Err(Error::new(ErrorKind::InvalidData)
                    .with_description(format!("Invalid enum repository ID for making Tango command data: {repo_id}
                        Only accepting DevState for TK_ENUM")))
            }
            read_scalar::<DevState>(buf)?
        }
        TK_STRUCT => {
            // tk_struct: only the Tango composite types, the two numeric and string arrays
            // and the encoded value.
            let repo_id = buf.read_encapsulation()?.read_string()?;
            match &*repo_id {
                tango_repo_id!("DevVarLongStringArray") => Value::LongString(read_seq(buf)?, read_seq(buf)?),
                tango_repo_id!("DevVarDoubleStringArray") => Value::DoubleString(read_seq(buf)?, read_seq(buf)?),
                tango_repo_id!("DevEncoded") => Value::Encoded(read_encoded(buf)?),
                repo_id => return Err(Error::new(ErrorKind::InvalidData)
                    .with_description(format!("Invalid structure repository ID for making a Tango command data: {repo_id}
                        Only accepting DevVarLongStringArray, DevVarDoubleStringArray or DevEncoded"))),
            }
        }
        TK_ALIAS => {
            // tk_alias, whose parameters are an encapsulation read in place.
            let repo_id = buf.read_encapsulation()?.read_string()?;
            match &*repo_id {
                tango_repo_id!("DevVarBooleanArray") => read_spectrum::<bool>(buf)?,
                tango_repo_id!("DevVarCharArray") => read_spectrum::<u8>(buf)?,
                tango_repo_id!("DevVarUShortArray") => read_spectrum::<u16>(buf)?,
                tango_repo_id!("DevVarShortArray") => read_spectrum::<i16>(buf)?,
                tango_repo_id!("DevVarULongArray") => read_spectrum::<u32>(buf)?,
                tango_repo_id!("DevVarLongArray") => read_spectrum::<i32>(buf)?,
                tango_repo_id!("DevVarULong64Array") => read_spectrum::<u64>(buf)?,
                tango_repo_id!("DevVarLong64Array") => read_spectrum::<i64>(buf)?,
                tango_repo_id!("DevVarFloatArray") => read_spectrum::<f32>(buf)?,
                tango_repo_id!("DevVarDoubleArray") => read_spectrum::<f64>(buf)?,
                tango_repo_id!("DevVarStateArray") => read_spectrum::<DevState>(buf)?,
                tango_repo_id!("DevVarStringArray") => read_spectrum::<str>(buf)?,
                repo_id => return Err(Error::new(ErrorKind::InvalidData)
                    .with_description(format!("Invalid alias repository ID for making Tango command data: {repo_id}
                        Only accepting DevVar*Array for TK_ALIAS"))),
            }
        }
        tk => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid typecode kind for making Tango command data: {tk}"))),
    })

}

// ================= //
//     Attribute     //
// ================= //

/// Common function for all versions of attribute reading.
///
/// The IDL only has four of these structures for the six device versions: v2 reuses the
/// v1 one and v6 the v5 one, so what changes from one version to the next is
/// - v3 gains a separate pair of dimensions for the read and the write values, which
///   tells a read-only attribute from a write-only one, and an error list per attribute;
/// - v4 replaces the CORBA::Any with a discriminated union, which is much easier to
///   decode, and announces the data format instead of leaving it to be guessed;
/// - v5 adds a data_type field, the declared type of the attribute, which the arm carrying
///   the value only tells apart from for a DevEnum, a short arm being the one both of them
///   travel in. It would be nice to check the two against each other at some point, to
///   ensure coherency!
pub fn read_attr(buf: &mut CdrReader<'_>, version: DevVersion) -> Result<NamedResult<AttrRead<Value>>> {

    // The values of both directions, as the single sequence they travel in, which only the
    // dimensions that come after them tell how to split.
    let mut values = if version <= DevVersion::V3 {
        read_attr_values_any(buf)?.map(AttrValues::Flat)
    } else {
        read_attr_values_union(buf)?
    };

    let quality = buf.read::<AttrQuality>()?;
    if quality == AttrQuality::Invalid {
        values = None;
    }

    let mut format = None;
    if version >= DevVersion::V4 {
        format = read_attr_data_format(buf)?;
    }

    if version >= DevVersion::V5 {

        let data_type = buf.read_long()?;

        // A DevEnum travels in the very same short arm a DevShort does, the declared type
        // being the only thing telling the two apart, so the values that were just read are
        // relabelled here, sharing the very same octets. Note that this is only ever
        // possible from v5 on, which is both the version that declares the type along the
        // value and the one DevEnum appeared with.
        if data_type == DEV_ENUM
        && let Some(AttrValues::Flat(Value::Short(ValueFormat::Spectrum(shorts)))) = &values {
            let enums = shorts.clone().cast::<DevEnum, 1>(shorts.dim());
            values = Some(AttrValues::Flat(Value::from(enums.expect("a short is a valid enumeration"))));
        }

    }

    let time = buf.read::<SystemTime>()?;

    // Shared out of the reply rather than borrowed from it, so that it outlives the reader.
    let name = buf.read::<Scalar<str>>()?;
    let r_x = buf.read_sequence_len()?;
    let r_y = buf.read_sequence_len()?;

    let mut w_x = 0;
    let mut w_y = 0;
    let mut err = None;
    if version >= DevVersion::V3 {
        w_x = buf.read_sequence_len()?;
        w_y = buf.read_sequence_len()?;
        err = buf.read::<Option<Error>>()?;
    }

    Ok(Named::new(
        name,
        if let Some(err) = err {
            Err(err)
        } else {
            let (value, w_value) = match values {
                Some(AttrValues::Flat(values)) => split_attr_values(Some(values), format, (r_x, r_y), (w_x, w_y)),
                Some(AttrValues::Encoded(parts)) => split_attr_encoded(parts, format, (r_x, r_y), (w_x, w_y)),
                None => (None, None),
            };
            Ok(AttrRead {
                value,
                w_value,
                quality,
                time,
            })
        }
    ))

}

/// Common function for all versions of attribute writing, the exact mirror of
/// [`read_attr`]: every field of the structure goes out, and not only the ones a client
/// write is read back for, because this is also what a server answers a reading with.
///
/// Note that a client only ever sends two of those four structures, `write_attributes_3`
/// taking the v1 one instead of the v3 one and no v5 variant of the operation having ever
/// been added, but a server has to answer a reading with the one its version declares.
pub fn write_attr(buf: &mut CdrWriter, version: DevVersion, name: &str, read: &Result<AttrRead<Value>>) -> Result<()> {

    // A failed reading has no value, no quality and no timestamp to send, so it goes out
    // the way a server sends one: nothing in the value, an invalid quality, zeroed
    // dimensions and the failure itself in the error list.
    let (value, w_value, quality, time) = match read {
        Ok(read) => (read.value.as_ref(), read.w_value.as_ref(), read.quality, read.time),
        Err(_) => (None, None, AttrQuality::Invalid, SystemTime::UNIX_EPOCH),
    };

    let (
        format, 
        data_type, 
        r_dim, 
        w_dim,
    ) = write_attr_data(buf, version <= DevVersion::V3, value, w_value)?;

    buf.write(&quality)?;

    if version >= DevVersion::V4 {
        write_attr_data_format(buf, format)?;
    }

    if version >= DevVersion::V5 {
        buf.write_long(data_type)?;
    }

    buf.write(&time)?;
    buf.write_string(name)?;

    // v1 has a single pair of dimensions, having neither a written one nor a place to
    // report a failure of its own, and that pair describes whichever value the structure
    // carries: a client write carries only the written one, so those are its dimensions
    // that go out, and a server answering a reading carries the read one.
    if version >= DevVersion::V3 {
        // A structure that only carries a written value still announces the dimensions of
        // what it carries on both sides, which is what cppTango sends and therefore what
        // the reading side of this implementation expects, see [`read_attr`]. The written
        // dimensions are the ones that mean anything here, the read ones only saying that
        // the value is there.
        let r_dim = if value.is_none() && w_value.is_some() { w_dim } else { r_dim };
        write_attr_dim(buf, r_dim.0, r_dim.1)?;
        write_attr_dim(buf, w_dim.0, w_dim.1)?;
        buf.write(&read.as_ref().err())?;
    } else {
        let (dim_x, dim_y) = if value.is_some() { r_dim } else { w_dim };
        write_attr_dim(buf, dim_x, dim_y)?;
    }

    Ok(())

}

fn read_attr_values_any(buf: &mut CdrReader<'_>) -> Result<Option<Value>> {

    let tk = buf.read_ulong()?;
    if tk == TK_NULL || tk == TK_VOID {
        return Ok(None);
    } else if tk == TK_ENUM {
        let repo_id = buf.read_encapsulation()?.read_string()?;
        if repo_id != tango_repo_id!("DevState") {
            return Err(Error::new(ErrorKind::InvalidData)
                .with_description(format!("Invalid enum repository ID for making Tango attribute data: {repo_id}
                    Only accepting DevState for TK_ENUM")))
        }
        return DevState::read_values(buf, 1).map(|state| Some(Value::from(state)));
    } else if tk != TK_ALIAS {
        return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid typecode kind for making Tango attribute data: {tk}")));
    }

    let repo_id = buf.read_encapsulation()?.read_string()?;

    let kind = match &*repo_id {
        tango_repo_id!("DevVarBooleanArray") => AttrDataKind::Bool,
        tango_repo_id!("DevVarCharArray") => AttrDataKind::Octet,
        tango_repo_id!("DevVarUShortArray") => AttrDataKind::UShort,
        tango_repo_id!("DevVarShortArray") => AttrDataKind::Short,
        tango_repo_id!("DevVarULongArray") => AttrDataKind::ULong,
        tango_repo_id!("DevVarLongArray") => AttrDataKind::Long,
        tango_repo_id!("DevVarULong64Array") => AttrDataKind::ULong64,
        tango_repo_id!("DevVarLong64Array") => AttrDataKind::Long64,
        tango_repo_id!("DevVarFloatArray") => AttrDataKind::Float,
        tango_repo_id!("DevVarDoubleArray") => AttrDataKind::Double,
        tango_repo_id!("DevVarStateArray") => AttrDataKind::State,
        tango_repo_id!("DevVarStringArray") => AttrDataKind::String,
        repo_id => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid alias repository ID for making Tango data: {repo_id}
                Only accepting DevVar*Array for TK_ALIAS"))),
    };

    read_attr_values(buf, kind).map(Some)

}

/// The values of both directions of an attribute, as the single sequence they travel in.
enum AttrValues {
    /// A sequence of elements, which the dimensions tell how to split, see
    /// [`split_attr_values`].
    Flat(Value),
    /// A sequence of encoded values, one per direction, see [`split_attr_encoded`].
    Encoded(Vec<DevEncoded>),
}

fn read_attr_values_union(buf: &mut CdrReader<'_>) -> Result<Option<AttrValues>> {

    let discriminant = buf.read_ulong()?;
    let kind = match discriminant {
        ATT_BOOL => AttrDataKind::Bool,
        ATT_SHORT => AttrDataKind::Short,
        ATT_LONG => AttrDataKind::Long,
        ATT_LONG64 => AttrDataKind::Long64,
        ATT_FLOAT => AttrDataKind::Float,
        ATT_DOUBLE => AttrDataKind::Double,
        ATT_UCHAR => AttrDataKind::Octet,
        ATT_USHORT => AttrDataKind::UShort,
        ATT_ULONG => AttrDataKind::ULong,
        ATT_ULONG64 => AttrDataKind::ULong64,
        ATT_STRING => AttrDataKind::String,
        ATT_STATE => AttrDataKind::State,
        // A single state, and not a sequence of them.
        ATT_DEVICE_STATE => return DevState::read_values(buf, 1).map(|state| Some(AttrValues::Flat(Value::from(state)))),
        ATT_ENCODED => {
            // Not reserved upfront, the length being only what the message claims.
            let len = buf.read_sequence_len()?;
            let mut parts = Vec::new();
            for _ in 0..len {
                parts.push(read_encoded(buf)?);
            }
            return Ok(Some(AttrValues::Encoded(parts)));
        }
        ATT_NO_DATA => {
            let _no_data = buf.read_bool()?;
            return Ok(None);
        }
        _ => return Err(Error::not_implemented()
            .with_append_description(format_args!("Unknown attribute value union discriminant: {discriminant}"))),
    };

    read_attr_values(buf, kind).map(|values| Some(AttrValues::Flat(values)))

}

/// Read the sequence the values of an attribute travel in, which is the same whether the
/// `any` of v1 or the union of v4 announced it.
fn read_attr_values(buf: &mut CdrReader<'_>, kind: AttrDataKind) -> Result<Value> {
    match kind {
        AttrDataKind::Bool => read_spectrum::<bool>(buf),
        AttrDataKind::Octet => read_spectrum::<u8>(buf),
        AttrDataKind::UShort => read_spectrum::<u16>(buf),
        AttrDataKind::Short => read_spectrum::<i16>(buf),
        AttrDataKind::ULong => read_spectrum::<u32>(buf),
        AttrDataKind::Long => read_spectrum::<i32>(buf),
        AttrDataKind::ULong64 => read_spectrum::<u64>(buf),
        AttrDataKind::Long64 => read_spectrum::<i64>(buf),
        AttrDataKind::Float => read_spectrum::<f32>(buf),
        AttrDataKind::Double => read_spectrum::<f64>(buf),
        AttrDataKind::Enum => read_spectrum::<DevEnum>(buf),
        AttrDataKind::State => read_spectrum::<DevState>(buf),
        AttrDataKind::String => read_spectrum::<str>(buf),
        AttrDataKind::Encoded => Err(Error::new(ErrorKind::InvalidData)
            .with_description("Encoded attribute values are not a sequence of elements")),
    }
}

fn read_attr_data_format<'a>(buf: &mut CdrReader<'a>) -> Result<Option<AttrDataFormat>> {
    Ok(Some(match buf.read_ulong()? {
        FMT_SCALAR => AttrDataFormat::Scalar,
        FMT_SPECTRUM => AttrDataFormat::Spectrum(0),
        FMT_IMAGE => AttrDataFormat::Image(0, 0),
        FMT_UNKNOWN => return Ok(None),
        value => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid attribute data format value: {value}")))
    }))
}

/// Note that the absence of a format is `FMT_UNKNOWN`, which is what goes out along a
/// value the device has nothing to put in.
fn write_attr_data_format(buf: &mut CdrWriter, format: Option<AttrDataFormat>) -> Result<()> {
    buf.write_ulong(match format {
        Some(AttrDataFormat::Scalar) => FMT_SCALAR,
        Some(AttrDataFormat::Spectrum(_)) => FMT_SPECTRUM,
        Some(AttrDataFormat::Image(_, _)) => FMT_IMAGE,
        None => FMT_UNKNOWN,
    })
}

/// Write the values of one direction of an attribute, without the length that precedes
/// the sequence both of them share, and give back what it announces.
fn write_attr_values(buf: &mut CdrWriter, value: Option<&Value>) -> Result<(Option<AttrDataFormat>, (usize, usize), usize)> {

    fn part<T: CdrTangoElement + ?Sized>(buf: &mut CdrWriter, format: &ValueFormat<T>) -> Result<(Option<AttrDataFormat>, (usize, usize), usize)> {
        let (format, dim, len) = match format {
            ValueFormat::Scalar(value) => {
                T::write_values(buf, value)?;
                (AttrDataFormat::Scalar, (1, 0), 1)
            }
            ValueFormat::Spectrum(values) => {
                T::write_values(buf, values)?;
                (AttrDataFormat::Spectrum(0), (values.len(), 0), values.len())
            }
            ValueFormat::Image(values) => {
                T::write_values(buf, values)?;
                let [x, y] = values.dim();
                (AttrDataFormat::Image(0, 0), (x, y), values.len())
            }
        };
        Ok((Some(format), dim, len))
    }

    let Some(value) = value else {
        return Ok((None, (0, 0), 0));
    };

    match value {
        Value::Bool(format) => part(buf, format),
        Value::Octet(format) => part(buf, format),
        Value::UShort(format) => part(buf, format),
        Value::Short(format) => part(buf, format),
        Value::ULong(format) => part(buf, format),
        Value::Long(format) => part(buf, format),
        Value::ULong64(format) => part(buf, format),
        Value::Long64(format) => part(buf, format),
        Value::Float(format) => part(buf, format),
        Value::Double(format) => part(buf, format),
        Value::Enum(format) => part(buf, format),
        Value::State(format) => part(buf, format),
        Value::String(format) => part(buf, format),
        value => Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("The given data could not be encoded as a Tango attribute value: {value:?}"))),
    }

}

/// Write the value of an attribute, which is a single sequence holding both directions:
/// the read part comes first and the write part right after it, only the two pairs of
/// dimensions telling them apart. Returns what the enclosing structure has to announce.
fn write_attr_data(
    buf: &mut CdrWriter,
    any: bool,
    value: Option<&Value>,
    w_value: Option<&Value>,
) -> Result<(Option<AttrDataFormat>, i32, (usize, usize), (usize, usize))> {

    /// How the element type of that value is announced, see [`WireElement`].
    fn codes<T: CdrTangoElement + ?Sized>(_: &ValueFormat<T>) -> (u32, &'static str, u32, i32) {
        (T::TK, T::ARRAY_REPO_ID, T::ATT, T::DEV_TYPE)
    }

    // Both directions share the type of the attribute, so whichever one is there tells
    // what the value is made of, and neither of them is a value the device has nothing to
    // put in: the Any of v1 then stays empty and the union of v4 picks its no-data arm.
    let Some(first) = value.or(w_value) else {
        if any {
            buf.write_ulong(TK_NULL)?;
        } else {
            buf.write_ulong(ATT_NO_DATA)?;
            buf.write_bool(false)?;
        }
        return Ok((None, DATA_TYPE_UNKNOWN, (0, 0), (0, 0)));
    };

    // Sharing the type also means that a read-write attribute cannot have one of each,
    // which would otherwise go out announced as the type of its read value only.
    if let (Some(value), Some(w_value)) = (value, w_value)
    && mem::discriminant(value) != mem::discriminant(w_value) {
        return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("The two directions of an attribute must share their type: {value:?} and {w_value:?}")));
    }

    if let Value::Encoded(_) = first {
        return write_attr_encoded(buf, any, value, w_value);
    }

    let (tk, array_repo_id, discriminant, data_type) = match first {
        Value::Bool(format) => codes(format),
        Value::Octet(format) => codes(format),
        Value::UShort(format) => codes(format),
        Value::Short(format) => codes(format),
        Value::ULong(format) => codes(format),
        Value::Long(format) => codes(format),
        Value::ULong64(format) => codes(format),
        Value::Long64(format) => codes(format),
        Value::Float(format) => codes(format),
        Value::Double(format) => codes(format),
        Value::Enum(format) => codes(format),
        Value::State(format) => codes(format),
        Value::String(format) => codes(format),
        value => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("The given data could not be encoded as a Tango attribute value: {value:?}"))),
    };

    if any {
        // The Any of v1 always aliases a DevVar*Array, even for a scalar.
        write_array_tc(buf, tk, array_repo_id)?;
    } else {
        buf.write_ulong(discriminant)?;
    }

    // The length of the sequence is only known once both parts are out, so it is left
    // behind and patched afterward, the way a message length is, see `WriteMessageLen`.
    let len_pos = buf.position();
    buf.write_sequence_len(0)?;

    let (r_format, r_dim, r_len) = write_attr_values(buf, value)?;
    let (w_format, w_dim, w_len) = write_attr_values(buf, w_value)?;

    let end = buf.position();
    buf.set_position(len_pos);
    buf.write_sequence_len(r_len + w_len)?;
    buf.set_position(end);

    // Both directions of an attribute always share its format too, so either one answers
    // for the pair and only their absence leaves it unknown.
    Ok((r_format.or(w_format), data_type, r_dim, w_dim))

}

/// Write the values of an encoded attribute, the sequence of one value per direction a
/// `DevVarEncodedArray` is, which only the union of v4 onward has an arm for: cppTango
/// refuses such an attribute to the clients of an earlier version, and so does this.
fn write_attr_encoded(
    buf: &mut CdrWriter,
    any: bool,
    value: Option<&Value>,
    w_value: Option<&Value>,
) -> Result<(Option<AttrDataFormat>, i32, (usize, usize), (usize, usize))> {

    if any {
        return Err(Error::new(ErrorKind::Reason(reason::API_NotSupportedFeature))
            .with_description("The DevEncoded data type is only available to devices of IDL 4 and above"));
    }

    // Both are encoded values or absent, the caller having checked that they share their type.
    fn encoded(value: Option<&Value>) -> Option<&DevEncoded> {
        match value {
            Some(Value::Encoded(encoded)) => Some(encoded),
            _ => None,
        }
    }

    let (value, w_value) = (encoded(value), encoded(w_value));

    buf.write_ulong(ATT_ENCODED)?;
    buf.write_sequence_len(value.is_some() as usize + w_value.is_some() as usize)?;

    let mut dim = |part: Option<&DevEncoded>| -> Result<(usize, usize)> {
        match part {
            Some(part) => write_encoded(buf, part).map(|()| (1, 0)),
            None => Ok((0, 0)),
        }
    };
    let r_dim = dim(value)?;
    let w_dim = dim(w_value)?;

    Ok((Some(AttrDataFormat::Scalar), DEV_ENCODED, r_dim, w_dim))

}

/// Write an `AttributeDim`, or the loose pair of dimensions v1 has in place of it, which
/// the IDL declares as longs and not as sequence lengths.
fn write_attr_dim(buf: &mut CdrWriter, dim_x: usize, dim_y: usize) -> Result<()> {
    let (Ok(dim_x), Ok(dim_y)) = (i32::try_from(dim_x), i32::try_from(dim_y)) else {
        return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Attribute dimensions do not fit into a long: {dim_x}x{dim_y}")));
    };
    buf.write_long(dim_x)?;
    buf.write_long(dim_y)
}

/// Cut the read and the written values of an attribute out of the single spectrum both
/// travel in, in that order, their formats being told by [`attr_formats`].
fn split_attr_values(
    values: Option<Value>,
    format: Option<AttrDataFormat>,
    r_dim: (usize, usize),
    w_dim: (usize, usize),
) -> (Option<Value>, Option<Value>) {

    fn split<T: TangoElement + ?Sized>(
        values: ValueFormat<T>,
        format: Option<AttrDataFormat>,
        r_dim: (usize, usize),
        w_dim: (usize, usize),
    ) -> (Option<Value>, Option<Value>) {

        let Ok(values) = values.into_spectrum() else {
            return (None, None);
        };

        let (r_format, w_format) = attr_formats(format, r_dim, w_dim, values.len());

        // Every part shares the octets of the whole, and the formats account for exactly
        // as many values as there are, see [`attr_formats`].
        let mut at = 0;
        let mut take = |format: AttrDataFormat| {
            let len = match format {
                AttrDataFormat::Scalar => 1,
                AttrDataFormat::Spectrum(x) => x,
                AttrDataFormat::Image(x, y) => x * y,
            };
            let part = values.slice(at..at + len);
            at += len;
            T::wrap(match format {
                AttrDataFormat::Scalar => ValueFormat::Scalar(part.into_scalar().unwrap()),
                AttrDataFormat::Spectrum(_) => ValueFormat::Spectrum(part),
                AttrDataFormat::Image(x, y) => ValueFormat::Image(part.reshape([x, y]).unwrap()),
            })
        };

        (r_format.map(&mut take), w_format.map(take))

    }

    match values {
        Some(Value::Bool(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Octet(values)) => split(values, format, r_dim, w_dim),
        Some(Value::UShort(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Short(values)) => split(values, format, r_dim, w_dim),
        Some(Value::ULong(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Long(values)) => split(values, format, r_dim, w_dim),
        Some(Value::ULong64(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Long64(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Float(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Double(values)) => split(values, format, r_dim, w_dim),
        Some(Value::Enum(values)) => split(values, format, r_dim, w_dim),
        Some(Value::State(values)) => split(values, format, r_dim, w_dim),
        Some(Value::String(values)) => split(values, format, r_dim, w_dim),
        Some(_) | None => (None, None),
    }

}

/// Take the read and the written values of an encoded attribute out of the sequence both
/// travel in, one value each and in that order, their formats being told by
/// [`attr_formats`] like those of any other attribute.
fn split_attr_encoded(
    parts: Vec<DevEncoded>,
    format: Option<AttrDataFormat>,
    r_dim: (usize, usize),
    w_dim: (usize, usize),
) -> (Option<Value>, Option<Value>) {

    let (r_format, w_format) = attr_formats(format, r_dim, w_dim, parts.len());

    // An encoded value is only ever a scalar, so any other format leaves the part out.
    let mut parts = parts.into_iter();
    let mut take = |format: AttrDataFormat| match format {
        AttrDataFormat::Scalar => parts.next().map(Value::Encoded),
        _ => None,
    };

    (r_format.and_then(&mut take), w_format.and_then(take))

}

/// The formats, with their dimensions, of the read and the written values of an attribute
/// whose single spectrum holds `len` values, out of the format and the two pairs of
/// dimensions that were announced along it, both of them being absent when those make no
/// sense of that many values.
fn attr_formats(
    format: Option<AttrDataFormat>,
    r_dim: (usize, usize),
    w_dim: (usize, usize),
    len: usize,
) -> (Option<AttrDataFormat>, Option<AttrDataFormat>) {

    use AttrDataFormat::{Scalar, Spectrum, Image};

    match format {
        Some(AttrDataFormat::Scalar) => {
            match (r_dim, w_dim, len) {
                ((1, 0), (0, 0), 1) => ( // Read-only
                    Some(Scalar),
                    None
                ),
                // The read dimensions are not looked at, the way they are not in the two
                // branches below: a structure carrying only a written value is described
                // by the written ones, and the read ones are whatever the sender left
                // there — see the note on the guessing branch.
                ((_, _), (1, 0), 1) => ( // Write-only
                    None,
                    Some(Scalar),
                ),
                ((1, 0), (1, 0), 2) => ( // Read-write
                    Some(Scalar),
                    Some(Scalar),
                ),
                _ => (None, None)
            }
        }
        Some(AttrDataFormat::Spectrum(_)) => {
            match (r_dim, w_dim) {
                ((r_x, 0), (0, 0)) if r_x == len => ( // Read-only
                    Some(Spectrum(r_x)),
                    None,
                ),
                ((_, 0), (w_x, 0)) if w_x == len => ( // Write-only
                    None,
                    Some(Spectrum(w_x)),
                ),
                ((r_x, 0), (w_x, 0)) if r_x + w_x == len => ( // Read-write
                    Some(Spectrum(r_x)),
                    Some(Spectrum(w_x)),
                ),
                _ => (None, None)
            }
        }
        Some(AttrDataFormat::Image(_, _)) => {
            match (r_dim, w_dim) {
                ((r_x, r_y), (0, 0)) if r_x * r_y == len => ( // Read-only
                    Some(Image(r_x, r_y)),
                    None,
                ),
                ((_, _), (w_x, w_y)) if w_x * w_y == len => ( // Write-only
                    None,
                    Some(Image(w_x, w_y)),
                ),
                ((r_x, r_y), (w_x, w_y)) if r_x * r_y + w_x * w_y == len => ( // Read-write
                    Some(Image(r_x, r_y)),
                    Some(Image(w_x, w_y)),
                ),
                _ => (None, None)
            }
        }
        None => {

            // We must guess the data format from the dimensions, note that this is used
            // for read_attributes v1 and v3, note that v1 don't even have different 
            // dimensions for read and write.
            //
            // What's known in v1:
            // - Not possible to distinguish read-only from write-only for scalars;
            // - Can only read read-only spectrums or images;
            //
            // It is also the branch a write request from cppTango lands in, which is what
            // the three write-only arms come first for. Such a request announces
            // `FMT_UNKNOWN` and fills in the written dimensions, and leaves the read ones
            // as it found them: `AttributeDim` is generated without a constructor and a
            // client never sets that pair, so they are the halves of a pointer as often as
            // not. They are therefore not looked at where the written ones already account
            // for the whole sequence — reading them cost a scalar its shape, a garbage
            // `r_dim` with a zero `dim_y` matching the spectrum arm and making a spectrum
            // of one out of it, and cost it its value outright otherwise.

            match (r_dim, w_dim) {
                ((_, _), (1, 0)) if len == 1 => ( // Write-only, and only that
                    None,
                    Some(Scalar),
                ),
                ((_, _), (w_x, 0)) if w_x == len => ( // Write-only spectrum
                    None,
                    Some(Spectrum(w_x)),
                ),
                ((_, _), (w_x, w_y)) if w_x * w_y == len => ( // Write-only image
                    None,
                    Some(Image(w_x, w_y)),
                ),
                ((1, 0), (0, 0)) if len == 1 => ( // Read-only
                    Some(Scalar),
                    None,
                ),
                ((1, 0), (0, 0) | (1, 0)) if len == 2 => ( // Read-write
                    Some(Scalar),
                    Some(Scalar),
                ),
                ((r_x, 0), (0, 0)) if r_x == len => ( // Read-only
                    Some(Spectrum(r_x)),
                    None,
                ),
                ((r_x, 0), (w_x, 0)) if r_x + w_x == len => ( // Read-write (v3 only)
                    Some(Spectrum(r_x)),
                    Some(Spectrum(w_x)),
                ),
                ((r_x, r_y), (0, 0)) if r_x * r_y == len => ( // Read-only
                    Some(Image(r_x, r_y)),
                    None,
                ),
                ((r_x, r_y), (w_x, w_y)) if r_x * r_y + w_x * w_y == len => ( // Read-write (v3 only)
                    Some(Image(r_x, r_y)),
                    Some(Image(w_x, w_y)),
                ),
                _ => (None, None)
            }

        }
    }

}

// =================== //
//  Command/attr info  //
// =================== //

/// The error to report for a data type that none of our data kinds has a variant for,
/// which is either one we know of and have yet to support, pipe blobs being the one a
/// device really sends, or one that no Tango data type has that value. Note that cppTango
/// has no use of `DevVarEncodedArray` but as the sequence an encoded attribute travels in.
///
/// Note that this only makes the one command or attribute carrying it unusable and not the
/// whole listing it belongs to, which is why the info readers give it back for that one
/// object instead of failing, see [`read_command_info`] and [`read_attr_info`].
fn unsupported_data_type_err(data_type: i32) -> Error {
    match data_type {
        DEVVAR_ENCODEDARRAY => Error::not_implemented()
            .with_append_description("DevVarEncodedArray data"),
        DEV_PIPE_BLOB => Error::not_implemented()
            .with_append_description("Pipe blob data"),
        data_type => Error::new(ErrorKind::InvalidData)
            .with_description(format!("Unknown Tango data type: {data_type}")),
    }
}

/// Read a `DispLevel`, which every version of the info structures has but v1 of the command
/// one. Note that a device sends `DL_UNKNOWN` for an object it has no level for, which
/// cppTango shows at the operator level, and so do we, [`DisplayLevel`] having no variant
/// for an unknown level.
fn read_display_level(buf: &mut CdrReader<'_>) -> Result<DisplayLevel> {
    Ok(match buf.read_ulong()? {
        DL_OPERATOR | DL_UNKNOWN => DisplayLevel::Operator,
        DL_EXPERT => DisplayLevel::Expert,
        value => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid display level value: {value}"))),
    })
}

/// Read a property, giving it back only if the device really has a value for it, see the
/// placeholders above. The one to expect depends on the property, hence the parameter.
fn read_prop<'a>(buf: &mut CdrReader<'a>, unset: &str) -> Result<Option<Cow<'static, str>>> {
    let value = buf.read_string()?;
    Ok(if value.is_empty() || value == unset {
        None
    } else {
        // The configuration outlives the buffer it was read from, so what a device sends
        // is always owned; only what a server defines by itself is borrowed.
        Some(Cow::Owned(value.into_owned()))
    })
}

/// Read one of the numeric properties, the bounds of a range and the deltas, which a device
/// leaves unset either the way any property is or with a not-a-number. What is left is the
/// value itself, still as the string the IDL declares it as.
fn read_value_str<'a>(buf: &mut CdrReader<'a>) -> Result<Option<Cow<'a, str>>> {
    let value = buf.read_string()?;
    Ok(if value.is_empty() || value == NOT_SPECIFIED || value == NOT_A_NUMBER {
        None
    } else {
        Some(value)
    })
}

/// Read one of those properties as the double it is, which is what the criteria of the
/// events are: a change is a magnitude and a relative change a percentage, neither of them
/// a value of the attribute, and Tango holds both as doubles whatever the attribute is.
///
/// A value that is not a number we can compare against — one a device wrote in a way we
/// cannot read, or an infinity — is one we have nothing to report, which is what an unset
/// one gives too, so it is not worth failing the whole configuration for.
fn read_value_prop<'a>(buf: &mut CdrReader<'a>) -> Result<Option<f64>> {
    Ok(read_value_str(buf)?
        .and_then(|value| value.trim().parse::<f64>().ok())
        .filter(|value| value.is_finite()))
}

/// Read one of those properties as a limit, which is a value of the attribute itself and
/// is therefore read as the type of the attribute, see [`AttrNumber`].
///
/// A device that wrote a limit its own type cannot hold still has one, so what does not
/// parse as the type of the attribute is read as a double before being given up on.
fn read_limit_prop<'a>(buf: &mut CdrReader<'a>, kind: LimitKind) -> Result<Option<AttrNumber>> {

    let Some(value) = read_value_str(buf)? else {
        return Ok(None);
    };
    let value = value.trim();

    let typed = match kind {
        LimitKind::Long => value.parse::<i64>().ok().map(AttrNumber::Long),
        LimitKind::ULong => value.parse::<u64>().ok().map(AttrNumber::ULong),
        LimitKind::Double => None,
    };

    Ok(typed.or_else(|| value.parse::<f64>().ok()
        .filter(|value| value.is_finite())
        .map(AttrNumber::Double)))

}

/// Which of the three numbers of an [`AttrNumber`] the limits of an attribute of that data
/// type are, which is only the signedness of it: the limits of an attribute are values of
/// it, so they are of its own type.
fn limit_kind(data_type: i32) -> LimitKind {
    match data_type {
        DEV_SHORT | DEV_LONG | DEV_LONG64 => LimitKind::Long,
        DEV_UCHAR | DEV_USHORT | DEV_ULONG | DEV_ULONG64 => LimitKind::ULong,
        // The floating kinds, and the kinds that have no limits to speak of — a string, a
        // state, a boolean — whose properties a device leaves unset anyway.
        _ => LimitKind::Double,
    }
}

/// The three kinds of number the limits of an attribute may be read as, see [`limit_kind`].
#[derive(Debug, Clone, Copy)]
enum LimitKind {
    Double,
    Long,
    ULong,
}

/// Read one of the duration properties, which travel as a plain number in the unit the
/// property is defined in, which is milliseconds for every one of them, the event periods
/// and the RDS alarm delta time alike. A device that has no value for it leaves it unset
/// like any other property.
fn read_duration_prop<'a>(buf: &mut CdrReader<'a>, unit: Duration) -> Result<Option<Duration>> {
    let Some(value) = read_value_str(buf)? else {
        return Ok(None);
    };
    // A period the device sends as something other than a number is one we have nothing to
    // report, which is what an unset one gives too, so it is not worth failing the whole
    // configuration for.
    Ok(value.trim().parse::<u32>().ok().map(|value| unit * value))
}

/// The command data kind of a `CmdArgType`, which is what both the input and the output of
/// a command are announced with, the array enumerators included.
fn command_data_kind(data_type: i32) -> Result<CommandDataKind> {
    Ok(match data_type {
        DEV_VOID => CommandDataKind::Void,
        DEV_BOOLEAN => CommandDataKind::Bool,
        DEV_UCHAR => CommandDataKind::Octet,
        DEV_USHORT => CommandDataKind::UShort,
        DEV_SHORT => CommandDataKind::Short,
        DEV_ULONG => CommandDataKind::ULong,
        DEV_LONG => CommandDataKind::Long,
        DEV_ULONG64 => CommandDataKind::ULong64,
        DEV_LONG64 => CommandDataKind::Long64,
        DEV_FLOAT => CommandDataKind::Float,
        DEV_DOUBLE => CommandDataKind::Double,
        DEV_STATE => CommandDataKind::State,
        // A constant string is a string that the device promises not to free, which is a
        // C++ lifetime matter that changes nothing on the wire.
        DEV_STRING | CONST_DEV_STRING => CommandDataKind::String,
        // A DevEnum travels as a short and its labels only live in the configuration of an
        // attribute, so a command carrying one carries a short.
        DEV_ENUM => CommandDataKind::Short,
        DEVVAR_BOOLEANARRAY => CommandDataKind::BoolArray,
        DEVVAR_CHARARRAY => CommandDataKind::OctetArray,
        DEVVAR_USHORTARRAY => CommandDataKind::UShortArray,
        DEVVAR_SHORTARRAY => CommandDataKind::ShortArray,
        DEVVAR_ULONGARRAY => CommandDataKind::ULongArray,
        DEVVAR_LONGARRAY => CommandDataKind::LongArray,
        DEVVAR_ULONG64ARRAY => CommandDataKind::ULong64Array,
        DEVVAR_LONG64ARRAY => CommandDataKind::Long64Array,
        DEVVAR_FLOATARRAY => CommandDataKind::FloatArray,
        DEVVAR_DOUBLEARRAY => CommandDataKind::DoubleArray,
        DEVVAR_STATEARRAY => CommandDataKind::StateArray,
        DEVVAR_STRINGARRAY => CommandDataKind::StringArray,
        DEVVAR_LONGSTRINGARRAY => CommandDataKind::LongStringArray,
        DEVVAR_DOUBLESTRINGARRAY => CommandDataKind::DoubleStringArray,
        DEV_ENCODED => CommandDataKind::Encoded,
        data_type => return Err(unsupported_data_type_err(data_type)),
    })
}

/// The attribute data kind of a `CmdArgType`, which is what the data type of an attribute
/// configuration holds, always the type of an element and never an array one whatever the
/// format of the attribute is.
fn attr_data_kind(data_type: i32) -> Result<AttrDataKind> {
    Ok(match data_type {
        DEV_BOOLEAN => AttrDataKind::Bool,
        DEV_UCHAR => AttrDataKind::Octet,
        DEV_USHORT => AttrDataKind::UShort,
        DEV_SHORT => AttrDataKind::Short,
        DEV_ULONG => AttrDataKind::ULong,
        DEV_LONG => AttrDataKind::Long,
        DEV_ULONG64 => AttrDataKind::ULong64,
        DEV_LONG64 => AttrDataKind::Long64,
        DEV_FLOAT => AttrDataKind::Float,
        DEV_DOUBLE => AttrDataKind::Double,
        DEV_STATE => AttrDataKind::State,
        DEV_STRING | CONST_DEV_STRING => AttrDataKind::String,
        // A DevEnum attribute travels as a short, the labels of its configuration being the
        // only thing telling it from one and giving a meaning to its values, which are the
        // indices in that list, see [`AttrConfig::enum_labels`].
        DEV_ENUM => AttrDataKind::Enum,
        DEV_ENCODED => AttrDataKind::Encoded,
        data_type => return Err(unsupported_data_type_err(data_type)),
    })
}

/// Common function for both versions of the command info structure, the v2 one only adding
/// the display level, which v1 has no place for at all: cppTango shows every command of a
/// v1 device at the operator level, and so do we.
///
/// Note that the `cmd_tag` both versions carry is not a display level despite holding small
/// numbers, it is a tag the IDL leaves to the device and that cppTango never reads back, so
/// it is skipped here.
pub fn read_command_info<'a>(buf: &mut CdrReader<'a>, version: DevVersion) -> Result<(Cow<'a, str>, Result<CommandInfo>)> {

    let name = buf.read_string()?;

    let display_level = if version >= DevVersion::V2 {
        read_display_level(buf)?
    } else {
        DisplayLevel::Operator
    };

    let _cmd_tag = buf.read_long()?;
    let in_type = buf.read_long()?;
    let out_type = buf.read_long()?;

    // The descriptions are read whatever the types are, so that the structure is fully
    // consumed even for a command we have no kind for, the next one in a list being read
    // right after it.
    let in_doc = read_prop(buf, UNINITIALIZED)?;
    let out_doc = read_prop(buf, UNINITIALIZED)?;

    let info = match (command_data_kind(in_type), command_data_kind(out_type)) {
        (Ok(in_kind), Ok(out_kind)) => Ok(CommandInfo {
            display_level,
            in_kind,
            out_kind,
            in_doc,
            out_doc,
        }),
        (Err(err), _) |
        (_, Err(err)) => Err(err),
    };

    Ok((name, info))

}

/// Common function for all versions of the attribute configuration structure.
///
/// The IDL only has four of these for the six device versions: v4 reuses the v3 one and v6
/// the v5 one, so what changes from one version to the next is
/// - v2 gains the display level;
/// - v3 moves the alarm bounds into a structure of their own, which also gains the warning
///   bounds and the delta of the read-different-from-set alarm, and gains the whole event
///   configuration along with a second list of extensions;
/// - v5 gains the memorization, the enumeration labels and the root attribute name of a
///   forwarded attribute.
///
/// Note that the version to give here is the one of the *structure* and not the one of the
/// device, which are not the same thing for a v4 device, whose configuration is the v3 one.
pub fn read_attr_info<'a>(buf: &mut CdrReader<'a>, version: DevVersion) -> Result<(Cow<'a, str>, Result<AttrInfo>)> {

    // Every field is read in the order the IDL declares it, and only assembled at the end,
    // so that the reading order stays plain to see.
    let name = buf.read_string()?;
    let writable = buf.read_ulong()?;
    let format = read_attr_data_format(buf)?;
    let data_type = buf.read_long()?;

    // The limits further down are values of the attribute, so they are read as its own
    // type, which the IDL is kind enough to announce before any of them.
    let limits = limit_kind(data_type);

    // v5 announces the memorization right after the type, the two flags being whether the
    // attribute is memorized at all and whether it is written back when the device starts.
    let mut memorized = None;
    if version >= DevVersion::V5 {
        memorized = match (buf.read_bool()?, buf.read_bool()?) {
            (false, _) => None,
            (true, false) => Some(AttrMemorized::RestoreSetPoint),
            (true, true) => Some(AttrMemorized::RestoreAndWrite),
        };
    }

    // The format the structure announces carries no dimension of its own, the maximum ones
    // the attribute is declared with being the two fields that follow it, and only the
    // formats that have dimensions have any use for them. A negative maximum is a device
    // that means none, the IDL declaring them as longs and not as sequence lengths.
    let max_dim_x = buf.read_long()?;
    let max_dim_y = buf.read_long()?;
    let max_dim = |value: i32| usize::try_from(value).unwrap_or(0);

    let data_format = match format {
        Some(AttrDataFormat::Scalar) => AttrDataFormat::Scalar,
        Some(AttrDataFormat::Spectrum(_)) => AttrDataFormat::Spectrum(max_dim(max_dim_x)),
        Some(AttrDataFormat::Image(_, _)) => AttrDataFormat::Image(max_dim(max_dim_x), max_dim(max_dim_y)),
        None => return Err(Error::new(ErrorKind::InvalidData)
            .with_description("The device sent an attribute of unknown data format")
            .with_append_description(format_args!("Attribute {name:?}"))),
    };

    let doc = read_prop(buf, NO_DESCRIPTION)?;
    let label = read_prop(buf, NO_LABEL)?;
    let unit = read_prop(buf, "")?;
    let standard_unit = read_prop(buf, NO_STANDARD_UNIT)?;
    let display_unit = read_prop(buf, NO_DISPLAY_UNIT)?;
    let display_format = read_prop(buf, NOT_SPECIFIED)?;
    let range = AttrRangeConfig {
        min: read_limit_prop(buf, limits)?,
        max: read_limit_prop(buf, limits)?,
    };

    // From here the versions part ways: v1 and v2 carry the alarm bounds on their own and
    // have nothing else, where v3 onward has the alarm structure and the events.
    let mut alarm = AttrRangeConfig::default();
    if version <= DevVersion::V2 {
        alarm = AttrRangeConfig {
            min: read_limit_prop(buf, limits)?,
            max: read_limit_prop(buf, limits)?,
        };
    }

    let write_attr_name = buf.read_string()?;

    // v1 is the only one with no display level, and it is the last field of v2 before the
    // extensions, where v3 onward has it right after the associated write attribute.
    let mut display_level = DisplayLevel::Operator;
    if version >= DevVersion::V2 {
        display_level = read_display_level(buf)?;
    }

    // Only v5 has these two, the root attribute naming what a forwarded attribute forwards,
    // which the data module has nowhere to hold for now, and the labels naming the values
    // of a DevEnum, which are what the data kind of one is made of.
    let mut enum_labels = Vec::new();
    if version >= DevVersion::V5 {
        let _root_attr_name = buf.read_string()?;
        enum_labels = buf.read::<Vec<String>>()?;
    }

    let mut warning = AttrRangeConfig::default();
    let mut delta = None;
    let mut change_event = AttrChangeEventConfig::default();
    let mut periodic_event = AttrPeriodicEventConfig::default();
    let mut archive_event = AttrArchiveEventConfig::default();

    if version >= DevVersion::V3 {

        // 'AttributeAlarm', which holds the bounds v1 and v2 have of their own along with
        // the ones they know nothing about.
        alarm = AttrRangeConfig {
            min: read_limit_prop(buf, limits)?,
            max: read_limit_prop(buf, limits)?,
        };
        warning = AttrRangeConfig {
            min: read_limit_prop(buf, limits)?,
            max: read_limit_prop(buf, limits)?,
        };
        let delta_time = read_duration_prop(buf, Duration::from_millis(1))?;
        let delta_value = read_limit_prop(buf, limits)?;
        let _alarm_extensions = buf.read::<Vec<Cow<'_, str>>>()?;

        // The delta of the read-different-from-set alarm is only an alarm at all if there
        // is a difference to compare, its time being how long the device waits first.
        delta = delta_value.map(|value| AttrDeltaConfig {
            value,
            time: delta_time,
        });

        // 'EventProperties', whose three structures each end with extensions of their own.
        change_event = AttrChangeEventConfig {
            rel_change: read_value_prop(buf)?,
            abs_change: read_value_prop(buf)?,
        };
        let _change_extensions = buf.read::<Vec<Cow<'_, str>>>()?;
        periodic_event = AttrPeriodicEventConfig {
            period: read_duration_prop(buf, Duration::from_millis(1))?,
        };
        let _periodic_extensions = buf.read::<Vec<Cow<'_, str>>>()?;
        archive_event = AttrArchiveEventConfig {
            rel_change: read_value_prop(buf)?,
            abs_change: read_value_prop(buf)?,
            period: read_duration_prop(buf, Duration::from_millis(1))?,
        };
        let _archive_extensions = buf.read::<Vec<Cow<'_, str>>>()?;

    }

    let extensions = buf.read::<Vec<String>>()?.into_iter()
        .map(Cow::Owned)
        .collect();

    // The second list of extensions is the one a device keeps for itself, which it is not
    // supposed to put anything in that a client would read.
    if version >= DevVersion::V3 {
        let _sys_extensions = buf.read::<Vec<Cow<'_, str>>>()?;
    }

    // The mode is the writability along with the attribute that a read-only one is the
    // reading of, which is the only case the name means anything in.
    let mode = match writable {
        WT_READ => AttrMode::Read,
        WT_READ_WITH_WRITE => AttrMode::ReadWithWrite {
            write_attr_name: Cow::Owned(write_attr_name.into_owned()),
        },
        WT_WRITE => AttrMode::Write,
        WT_READ_WRITE => AttrMode::ReadWrite,
        WT_UNKNOWN => return Err(Error::new(ErrorKind::InvalidData)
            .with_description("The device sent an attribute of unknown writability")
            .with_append_description(format_args!("Attribute {name:?}"))),
        value => return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Invalid attribute writability value: {value}"))),
    };

    let info = attr_data_kind(data_type).map(|data_kind| AttrInfo {
        config: AttrConfig {
            doc,
            label,
            unit,
            // The conversion factor travels as a text property rather than as one of the
            // numeric ones, so it is the one number here that is parsed out of a string a
            // device may have written anything in.
            standard_unit: standard_unit.and_then(|value| value.trim().parse::<f64>().ok()),
            display_unit,
            display_format,
            range,
            alarm,
            warning,
            delta,
            change_event,
            periodic_event,
            archive_event,
            enum_labels: enum_labels.into_iter().map(Cow::Owned).collect(),
            extensions,
        },
        mode,
        data_kind,
        data_format,
        display_level,
        memorized,
    });

    Ok((name, info))

}

/// Common function for all three versions of the device info structure.
///
/// The IDL has one of these for v1, v3 and v6, the others reusing the previous one, and
/// each adds a field at the end of the one before it: v3 the type of the device, and v6 the
/// versions of what the server is built out of.
///
/// Note that the version to give here is the one of the *structure* and not the one of the
/// device, a v4 and a v5 device both answering with the v3 one.
pub fn read_dev_info(buf: &mut CdrReader<'_>, version: DevVersion) -> Result<DevInfo> {

    // Every field is read in the order the IDL declares it, and only assembled at the end,
    // so that the reading order stays plain to see.
    let class = buf.read_string()?.into_owned();
    let server_id = buf.read_string()?.into_owned();
    let server_host = buf.read_string()?.into_owned();
    let server_version = buf.read_long()?;
    let doc_url = read_prop(buf, NOT_SPECIFIED)?;

    let mut dev_type = None;
    if version >= DevVersion::V3 {
        dev_type = read_prop(buf, NOT_SPECIFIED)?;
    }

    let mut version_info = Vec::new();
    if version >= DevVersion::V6 {
        let len = buf.read_sequence_len()?;
        version_info.try_reserve(len)
            .map_err(|_| Error::new(ErrorKind::Internal)
                .with_description(format!("Could not read the {len} versions of a device info")))?;
        for _ in 0..len {
            let key = buf.read_string()?.into_owned();
            let value = buf.read_string()?.into_owned();
            version_info.push((Cow::Owned(key), Cow::Owned(value)));
        }
    }

    Ok(DevInfo {
        class,
        server_id,
        server_host,
        server_version,
        doc_url,
        dev_type,
        version_info,
    })

}

/// The exact mirror of [`read_dev_info`], the version being the one of the structure the
/// info goes out in, which is the one of the operation that asked for it.
pub fn write_dev_info(buf: &mut CdrWriter, version: DevVersion, info: &DevInfo) -> Result<()> {

    buf.write_string(&info.class)?;
    buf.write_string(&info.server_id)?;
    buf.write_string(&info.server_host)?;
    buf.write_long(info.server_version)?;
    write_prop(buf, info.doc_url.as_deref(), NOT_SPECIFIED)?;

    if version >= DevVersion::V3 {
        write_prop(buf, info.dev_type.as_deref(), NOT_SPECIFIED)?;
    }

    // Only v6 has these, so a device that has some and is asked in an earlier version
    // simply does not send them, there being nowhere to put them.
    if version >= DevVersion::V6 {
        buf.write_sequence_len(info.version_info.len())?;
        for (key, value) in &info.version_info {
            buf.write_string(key)?;
            buf.write_string(value)?;
        }
    }

    Ok(())

}

/// Common function for both versions of the command info structure, the exact mirror of
/// [`read_command_info`].
///
/// Note that the version to give here is the one of the *structure* and not the one of the
/// device, every version from v2 onward answering with the v2 structure.
pub fn write_command_info(buf: &mut CdrWriter, version: DevVersion, name: &str, info: &CommandInfo) -> Result<()> {

    buf.write_string(name)?;

    if version >= DevVersion::V2 {
        write_display_level(buf, info.display_level)?;
    }

    // The tag is something the IDL leaves to the device and that no client reads back, so
    // this one has nothing to put in it, see [`read_command_info`].
    buf.write_long(0)?;

    buf.write_long(command_data_type(&info.in_kind))?;
    buf.write_long(command_data_type(&info.out_kind))?;

    write_prop(buf, info.in_doc.as_deref(), UNINITIALIZED)?;
    write_prop(buf, info.out_doc.as_deref(), UNINITIALIZED)?;

    Ok(())

}

/// Common function for all versions of the attribute configuration structure, the exact
/// mirror of [`read_attr_info`], whose comments say what changes from one version to the
/// next and in which order every field goes.
///
/// Note that the version to give here is the one of the *structure* and not the one of the
/// device, which are not the same thing for a v4 device, whose configuration is the v3 one.
pub fn write_attr_info(buf: &mut CdrWriter, version: DevVersion, name: &str, info: &AttrInfo) -> Result<()> {

    let AttrInfo {
        config,
        mode,
        data_kind,
        data_format,
        display_level,
        memorized,
    } = info;

    buf.write_string(name)?;

    buf.write_ulong(match mode {
        AttrMode::Read => WT_READ,
        AttrMode::ReadWithWrite { .. } => WT_READ_WITH_WRITE,
        AttrMode::Write => WT_WRITE,
        AttrMode::ReadWrite => WT_READ_WRITE,
    })?;

    write_attr_data_format(buf, Some(*data_format))?;

    let data_type = attr_data_type(data_kind);
    buf.write_long(data_type)?;

    // The limits further down are values of the attribute, so they go out as its own type,
    // the way they are read as it.
    let limits = limit_kind(data_type);

    if version >= DevVersion::V5 {
        buf.write_bool(memorized.is_some())?;
        buf.write_bool(*memorized == Some(AttrMemorized::RestoreAndWrite))?;
    }

    // Only the formats that have dimensions have any to declare, a scalar declaring none,
    // which is what cppTango sends for one as well.
    let (max_dim_x, max_dim_y) = match data_format {
        AttrDataFormat::Scalar => (1, 0),
        AttrDataFormat::Spectrum(max_x) => (*max_x, 0),
        AttrDataFormat::Image(max_x, max_y) => (*max_x, *max_y),
    };

    let (Ok(max_dim_x), Ok(max_dim_y)) = (i32::try_from(max_dim_x), i32::try_from(max_dim_y)) else {
        return Err(Error::new(ErrorKind::InvalidData)
            .with_description(format!("Maximum attribute dimensions do not fit into a long: {max_dim_x}x{max_dim_y}"))
            .with_append_description(format_args!("Attribute {name:?}")));
    };

    buf.write_long(max_dim_x)?;
    buf.write_long(max_dim_y)?;

    write_prop(buf, config.doc.as_deref(), NO_DESCRIPTION)?;
    write_prop(buf, config.label.as_deref(), NO_LABEL)?;
    write_prop(buf, config.unit.as_deref(), "")?;

    // The conversion factor travels as a text property rather than as one of the numeric
    // ones, so it goes out as the plain number it was parsed out of.
    match config.standard_unit {
        Some(factor) => buf.write_string(&factor.to_string())?,
        None => buf.write_string(NO_STANDARD_UNIT)?,
    }

    write_prop(buf, config.display_unit.as_deref(), NO_DISPLAY_UNIT)?;
    write_prop(buf, config.display_format.as_deref(), NOT_SPECIFIED)?;

    write_limit_prop(buf, config.range.min, limits)?;
    write_limit_prop(buf, config.range.max, limits)?;

    // v1 and v2 carry the alarm bounds on their own, where v3 onward has them in the alarm
    // structure further down, along with the ones they know nothing about.
    if version <= DevVersion::V2 {
        write_limit_prop(buf, config.alarm.min, limits)?;
        write_limit_prop(buf, config.alarm.max, limits)?;
    }

    // The associated write attribute only means anything for a read-only attribute that
    // has one, every other mode leaving it empty.
    buf.write_string(match mode {
        AttrMode::ReadWithWrite { write_attr_name } => write_attr_name,
        _ => "",
    })?;

    if version >= DevVersion::V2 {
        write_display_level(buf, *display_level)?;
    }

    if version >= DevVersion::V5 {
        // The root attribute of a forwarded attribute, which the data module has nowhere
        // to hold for now, so nothing here ever forwards anything.
        buf.write_string("")?;
        // The labels naming the values of a DevEnum, which every other kind has none of
        // and whose absence is what a client reads one of another kind back as.
        buf.write(&config.enum_labels)?;
    }

    if version >= DevVersion::V3 {

        // 'AttributeAlarm', which holds the bounds v1 and v2 have of their own along with
        // the ones they know nothing about.
        write_limit_prop(buf, config.alarm.min, limits)?;
        write_limit_prop(buf, config.alarm.max, limits)?;
        write_limit_prop(buf, config.warning.min, limits)?;
        write_limit_prop(buf, config.warning.max, limits)?;
        write_duration_prop(buf, config.delta.and_then(|delta| delta.time), Duration::from_millis(1))?;
        write_limit_prop(buf, config.delta.map(|delta| delta.value), limits)?;
        buf.write_sequence_len(0)?;  // Alarm extensions.

        // 'EventProperties', whose three structures each end with extensions of their own.
        write_value_prop(buf, config.change_event.rel_change)?;
        write_value_prop(buf, config.change_event.abs_change)?;
        buf.write_sequence_len(0)?;
        write_duration_prop(buf, config.periodic_event.period, Duration::from_millis(1))?;
        buf.write_sequence_len(0)?;
        write_value_prop(buf, config.archive_event.rel_change)?;
        write_value_prop(buf, config.archive_event.abs_change)?;
        write_duration_prop(buf, config.archive_event.period, Duration::from_millis(1))?;
        buf.write_sequence_len(0)?;

    }

    buf.write(&config.extensions)?;

    // The second list of extensions is the one a device keeps for itself, which a client
    // is not supposed to be told anything through.
    if version >= DevVersion::V3 {
        buf.write_sequence_len(0)?;
    }

    Ok(())

}

/// Write a `DispLevel`, the mirror of [`read_display_level`].
fn write_display_level(buf: &mut CdrWriter, level: DisplayLevel) -> Result<()> {
    buf.write_ulong(match level {
        DisplayLevel::Operator => DL_OPERATOR,
        DisplayLevel::Expert => DL_EXPERT,
    })
}

/// Write a property, an absent one going out as the placeholder a client reads back as no
/// value, the mirror of [`read_prop`].
fn write_prop(buf: &mut CdrWriter, value: Option<&str>, unset: &str) -> Result<()> {
    buf.write_string(value.unwrap_or(unset))
}

/// Write one of the numeric properties, the mirror of [`read_value_prop`]. An absent one
/// goes out as the placeholder every unset numeric property shares.
fn write_value_prop(buf: &mut CdrWriter, value: Option<f64>) -> Result<()> {
    match value {
        Some(value) => buf.write_string(&value.to_string()),
        None => buf.write_string(NOT_SPECIFIED),
    }
}

/// Write one of the limits, which is a value of the attribute itself and goes out written
/// the way the number it is written, the mirror of [`read_limit_prop`].
///
/// The kind is what the limit would be read back as, so a limit of another kind is written
/// as the number it is anyway: a client reads a limit its type cannot hold as a double
/// rather than losing it, the way this implementation does.
fn write_limit_prop(buf: &mut CdrWriter, value: Option<AttrNumber>, _kind: LimitKind) -> Result<()> {
    match value {
        Some(value) => buf.write_string(&value.to_string()),
        None => buf.write_string(NOT_SPECIFIED),
    }
}

/// Write one of the duration properties, as the plain number of the unit the property is
/// defined in that it travels as, the mirror of [`read_duration_prop`].
fn write_duration_prop(buf: &mut CdrWriter, value: Option<Duration>, unit: Duration) -> Result<()> {

    let Some(value) = value else {
        return buf.write_string(NOT_SPECIFIED);
    };

    // The properties are whole numbers of their unit, so a duration that is not one is
    // rounded to the nearest, and never down to zero, which would mean something else.
    let unit = unit.as_nanos().max(1);
    let count = (value.as_nanos() + unit / 2) / unit;

    buf.write_string(&count.max(1).to_string())

}

/// The `CmdArgType` of a command data kind, the mirror of [`command_data_kind`].
fn command_data_type(kind: &CommandDataKind) -> i32 {
    match kind {
        CommandDataKind::Void => DEV_VOID,
        CommandDataKind::Bool => DEV_BOOLEAN,
        CommandDataKind::Octet => DEV_UCHAR,
        CommandDataKind::UShort => DEV_USHORT,
        CommandDataKind::Short => DEV_SHORT,
        CommandDataKind::ULong => DEV_ULONG,
        CommandDataKind::Long => DEV_LONG,
        CommandDataKind::ULong64 => DEV_ULONG64,
        CommandDataKind::Long64 => DEV_LONG64,
        CommandDataKind::Float => DEV_FLOAT,
        CommandDataKind::Double => DEV_DOUBLE,
        CommandDataKind::State => DEV_STATE,
        CommandDataKind::String => DEV_STRING,
        CommandDataKind::BoolArray => DEVVAR_BOOLEANARRAY,
        CommandDataKind::OctetArray => DEVVAR_CHARARRAY,
        CommandDataKind::UShortArray => DEVVAR_USHORTARRAY,
        CommandDataKind::ShortArray => DEVVAR_SHORTARRAY,
        CommandDataKind::ULongArray => DEVVAR_ULONGARRAY,
        CommandDataKind::LongArray => DEVVAR_LONGARRAY,
        CommandDataKind::ULong64Array => DEVVAR_ULONG64ARRAY,
        CommandDataKind::Long64Array => DEVVAR_LONG64ARRAY,
        CommandDataKind::FloatArray => DEVVAR_FLOATARRAY,
        CommandDataKind::DoubleArray => DEVVAR_DOUBLEARRAY,
        CommandDataKind::StateArray => DEVVAR_STATEARRAY,
        CommandDataKind::StringArray => DEVVAR_STRINGARRAY,
        CommandDataKind::LongStringArray => DEVVAR_LONGSTRINGARRAY,
        CommandDataKind::DoubleStringArray => DEVVAR_DOUBLESTRINGARRAY,
        CommandDataKind::Encoded => DEV_ENCODED,
    }
}

/// The `CmdArgType` of an attribute data kind, the mirror of [`attr_data_kind`], which is
/// always the type of an element and never an array one whatever the format is.
fn attr_data_type(kind: &AttrDataKind) -> i32 {
    match kind {
        AttrDataKind::Bool => DEV_BOOLEAN,
        AttrDataKind::Octet => DEV_UCHAR,
        AttrDataKind::UShort => DEV_USHORT,
        AttrDataKind::Short => DEV_SHORT,
        AttrDataKind::ULong => DEV_ULONG,
        AttrDataKind::Long => DEV_LONG,
        AttrDataKind::ULong64 => DEV_ULONG64,
        AttrDataKind::Long64 => DEV_LONG64,
        AttrDataKind::Float => DEV_FLOAT,
        AttrDataKind::Double => DEV_DOUBLE,
        AttrDataKind::State => DEV_STATE,
        AttrDataKind::String => DEV_STRING,
        AttrDataKind::Enum => DEV_ENUM,
        AttrDataKind::Encoded => DEV_ENCODED,
    }
}

/// Equivalent to `NamedDevError` in the IDL, one entry of the `MultiDevFailed` exception
/// that the write operations raise when only some of the attributes failed.
#[derive(Debug)]
pub struct NamedError<'a> {
    /// Name of the attribute that failed to be written.
    pub name: Cow<'a, str>,
    /// Index of the attribute in the list the call was given.
    pub index: usize,
    /// The failure itself, absent when the server sent an empty error list for it.
    pub err: Option<Error>,
}

impl<'a> CdrRead<'a> for NamedError<'a> {
    fn read(buf: &mut CdrReader<'a>) -> Result<Self> {

        let name = buf.read_string()?;

        // 'index_in_call' is a long and not a sequence length, so a negative one is a
        // server that has no index to give rather than a huge one.
        let index = buf.read_long()?;
        let index = usize::try_from(index)
            .map_err(|_| Error::new(ErrorKind::InvalidData)
                .with_description(format!("Invalid attribute index in a MultiDevFailed: {index}")))?;

        Ok(Self {
            name,
            index,
            err: buf.read()?,
        })

    }
}

impl CdrWrite for NamedError<'_> {
    fn write(&self, buf: &mut CdrWriter) -> Result<()> {

        buf.write_string(&self.name)?;

        // 'index_in_call' is a long and not a sequence length, so an index that does not
        // fit in one goes out as the negative that means the server has none to give.
        buf.write_long(i32::try_from(self.index).unwrap_or(-1))?;

        buf.write(&self.err.as_ref())?;

        Ok(())

    }
}

#[cfg(test)]
mod tests {

    use super::*;

    use crate::value::Image;
    use super::super::orb::cdr::CdrWriter;

    /// Check the typecode of one of the two composite Tango types, the way the CORBA
    /// specification lays a `tk_struct` out: a repository ID, a name, the number of members,
    /// and then that many pairs of a member name and a member typecode.
    ///
    /// The parameters are checked to hold exactly that and nothing more, because a parameter
    /// that is missing does not shorten the stream, it makes every following one be read out
    /// of the wrong octets, which is what a server rejects the whole request for.
    fn check_composite_typecode(reader: &mut CdrReader<'_>, repo_id: &str, first_name: &str, first_repo_id: &str) {

        assert_eq!(reader.read_ulong().unwrap(), TK_STRUCT);

        let mut param = reader.read_encapsulation().unwrap();
        assert_eq!(param.read_string().unwrap(), repo_id);
        assert_eq!(param.read_string().unwrap(), type_name(repo_id));

        // The number of members, which used to be missing and made every server answer the
        // requests carrying one of these types with a marshalling error.
        assert_eq!(param.read_sequence_len().unwrap(), 2);

        assert_eq!(param.read_string().unwrap(), first_name);
        check_array_typecode(&mut param, first_repo_id);

        assert_eq!(param.read_string().unwrap(), "svalue");
        check_array_typecode(&mut param, tango_repo_id!("DevVarStringArray"));

        assert!(param.is_empty(), "the parameters of the structure hold more than the two members they declare");

    }

    /// Check the typecode of one of the Tango array types, which is a `tk_alias` naming a
    /// `tk_sequence` of the element type.
    fn check_array_typecode(reader: &mut CdrReader<'_>, repo_id: &str) {

        assert_eq!(reader.read_ulong().unwrap(), TK_ALIAS);

        let mut param = reader.read_encapsulation().unwrap();
        assert_eq!(param.read_string().unwrap(), repo_id);
        assert_eq!(param.read_string().unwrap(), type_name(repo_id));

        assert_eq!(param.read_ulong().unwrap(), TK_SEQUENCE);
        let mut sequence = param.read_encapsulation().unwrap();

        // The element type, of which only a string has a parameter of its own, the maximum
        // length that makes it a bounded one.
        if sequence.read_ulong().unwrap() == TK_STRING {
            assert_eq!(sequence.read_ulong().unwrap(), 0, "the strings should be unbounded");
        }

        assert_eq!(sequence.read_ulong().unwrap(), 0, "the sequence should be unbounded");
        assert!(sequence.is_empty(), "the parameters of the sequence hold more than an element type and a bound");
        assert!(param.is_empty(), "the parameters of the alias hold more than a name and a type");

    }

    /// The name a Tango repository ID gives the type, which is what it holds between the
    /// `IDL:Tango/` prefix and the `:1.0` version.
    fn type_name(repo_id: &str) -> &str {
        &repo_id[10..repo_id.len() - 4]
    }

    /// A `DevVarLongStringArray` command argument goes out as a `tk_struct` of two arrays,
    /// and comes back out of the very same octets.
    #[test]
    fn long_string_array() {

        let data = Value::from((Buf::from(&[1i32, 2, 3][..]), Buf::from(&["one", "two"][..])));

        let mut writer = CdrWriter::new();
        write_command_data(&mut writer, &data).unwrap();
        let buf = writer.into_buf().freeze();

        let mut reader = CdrReader::new(&buf);
        check_composite_typecode(
            &mut reader,
            tango_repo_id!("DevVarLongStringArray"),
            "lvalue",
            tango_repo_id!("DevVarLongArray"));

        assert_eq!(read_command_data(&mut CdrReader::new(&buf)).unwrap(), data);

    }

    /// A `DevEncoded` command argument goes out as the `tk_struct` the IDL declares, its
    /// format being a `DevString` alias, and comes back sharing the octets of the message.
    #[test]
    fn encoded_command() {

        let data = Value::from(DevEncoded::new("JPEG_RGB", vec![0xFFu8, 0xD8, 0xFF]));

        let mut writer = CdrWriter::new();
        write_command_data(&mut writer, &data).unwrap();
        let buf = writer.into_buf().freeze();

        let mut reader = CdrReader::new(&buf);
        assert_eq!(reader.read_ulong().unwrap(), TK_STRUCT);
        let mut param = reader.read_encapsulation().unwrap();
        assert_eq!(param.read_string().unwrap(), tango_repo_id!("DevEncoded"));
        assert_eq!(param.read_string().unwrap(), "DevEncoded");
        assert_eq!(param.read_sequence_len().unwrap(), 2);

        assert_eq!(param.read_string().unwrap(), "encoded_format");
        assert_eq!(param.read_ulong().unwrap(), TK_ALIAS);
        let mut alias = param.read_encapsulation().unwrap();
        assert_eq!(alias.read_string().unwrap(), tango_repo_id!("DevString"));
        assert_eq!(alias.read_string().unwrap(), "DevString");
        assert_eq!(alias.read_ulong().unwrap(), TK_STRING);
        assert_eq!(alias.read_ulong().unwrap(), 0, "the string should be unbounded");
        assert!(alias.is_empty(), "the parameters of the alias hold more than a name and a type");

        assert_eq!(param.read_string().unwrap(), "encoded_data");
        check_array_typecode(&mut param, tango_repo_id!("DevVarCharArray"));
        assert!(param.is_empty(), "the parameters of the structure hold more than the two members they declare");

        let back = read_command_data(&mut CdrReader::new(&buf)).unwrap();
        assert!(!buf.is_unique(), "the value was copied out of the message");
        assert_eq!(back, data);

    }

    /// An encoded reading goes out as the sequence of one value per direction it travels
    /// in, and comes back as it was, whichever directions it has.
    #[test]
    fn encoded_attr() {

        let read = Value::from(DevEncoded::new("JPEG_RGB", vec![1u8, 2, 3]));
        let written = Value::from(DevEncoded::new("RAW", vec![4u8]));

        for version in [DevVersion::V4, DevVersion::V5] {
            for (value, w_value) in [
                (Some(read.clone()), Some(written.clone())),
                (Some(read.clone()), None),
                (None, Some(written.clone())),
            ] {

                let read = AttrRead { value, w_value, quality: AttrQuality::Valid, time: SystemTime::UNIX_EPOCH };

                let mut writer = CdrWriter::new();
                write_attr(&mut writer, version, "attr", &Ok(read.clone())).unwrap();
                let buf = writer.into_buf().freeze();

                let back = read_attr(&mut CdrReader::new(&buf), version).unwrap().value;
                assert!(!buf.is_unique(), "the values were copied out of the reply");
                assert_eq!(back.unwrap(), read, "{version:?}");

            }
        }

    }

    /// An encoded reading cannot go out to a client of IDL 3 or earlier, which has no
    /// place for it, the way cppTango refuses it to them.
    #[test]
    fn encoded_attr_any() {
        let read = AttrRead::new(Value::from(DevEncoded::new("RAW", vec![1u8])));
        let mut writer = CdrWriter::new();
        assert!(write_attr(&mut writer, DevVersion::V3, "attr", &Ok(read)).is_err());
    }

    /// A write request from cppTango leaves its read dimensions uninitialised, so the
    /// written ones are what the value has to be shaped by: a scalar stays a scalar
    /// whatever is in the pair nobody set.
    ///
    /// It used to become a spectrum of one whenever the garbage happened to have a zero
    /// `dim_y`, and to be dropped altogether otherwise.
    #[test]
    fn written_scalar() {

        for r_dim in [(671181954, 32662), (3758189698, 0), (0, 0), (1, 0)] {

            let (value, w_value) = split_attr_values(Some(Value::from(vec![1e300])), None, r_dim, (1, 0));

            assert!(value.is_none(), "a write request carries no read value, {r_dim:?}");
            assert_eq!(w_value, Some(Value::from(1e300)), "with a read dim of {r_dim:?}");

        }

    }

    /// The same for a spectrum, whose length is the written dimension and not the pair
    /// that was never set.
    #[test]
    fn written_spectrum() {

        for r_dim in [(671181954, 32662), (3758189698, 0), (0, 0), (3, 0)] {

            let written = Value::from(vec![1.0, -2.0, 3.5]);
            let (value, w_value) = split_attr_values(Some(written.clone()), None, r_dim, (3, 0));

            assert!(value.is_none(), "a write request carries no read value, {r_dim:?}");
            assert_eq!(w_value, Some(written), "with a read dim of {r_dim:?}");

        }

    }

    /// What a read reply carries is still read by its own dimensions, both values of a
    /// read-write attribute included, which is what the arms above the write-only ones
    /// have to keep doing.
    #[test]
    fn read_values() {

        let (value, w_value) = split_attr_values(Some(Value::from(vec![1.5, 2.5])), None, (1, 0), (1, 0));
        assert_eq!(value, Some(Value::from(1.5)));
        assert_eq!(w_value, Some(Value::from(2.5)));

        let (value, w_value) = split_attr_values(Some(Value::from(vec![1.5])), None, (1, 0), (0, 0));
        assert_eq!(value, Some(Value::from(1.5)));
        assert!(w_value.is_none(), "a read-only attribute has no written value");

        let read = Value::from(vec![1.0, 2.0, 3.0]);
        let (value, w_value) = split_attr_values(Some(read.clone()), None, (3, 0), (0, 0));
        assert_eq!(value, Some(read));
        assert!(w_value.is_none(), "a read-only attribute has no written value");

    }

    /// A reading goes out and comes back as it was, its values sharing the octets of the
    /// reply: an image of both directions, and an enumeration, which only v5 tells from the
    /// short it travels as.
    #[test]
    fn attr_round_trip() {

        let image = Value::from(Image::<f64>::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [3, 2]).unwrap());
        let enums = Value::from(Spectrum::<DevEnum>::from(vec![DevEnum(1), DevEnum(2)]));

        for (version, value) in [(DevVersion::V4, image.clone()), (DevVersion::V5, image), (DevVersion::V5, enums)] {

            let read = AttrRead {
                value: Some(value.clone()),
                w_value: Some(value),
                quality: AttrQuality::Valid,
                time: SystemTime::UNIX_EPOCH,
            };

            let mut writer = CdrWriter::new();
            write_attr(&mut writer, version, "attr", &Ok(read.clone())).unwrap();
            let buf = writer.into_buf().freeze();

            let Named { name, value: back } = read_attr(&mut CdrReader::new(&buf), version).unwrap();
            let back = back.unwrap();
            assert_eq!(name.value(), "attr");
            assert!(!buf.is_unique(), "the values were copied out of the reply");
            assert_eq!(back, read, "{version:?}");

        }


    }

    /// An attribute configuration written and read back the same in every version of the
    /// structure, a client changing one sending it back as it read it, the durations being
    /// milliseconds on the wire.
    #[test]
    fn attr_info_round_trip() {

        let config = AttrConfig {
            unit: Some("mm".into()),
            range: AttrRangeConfig { min: Some(AttrNumber::Long(-5)), max: None },
            alarm: AttrRangeConfig { min: None, max: Some(AttrNumber::Long(10)) },
            delta: Some(AttrDeltaConfig { value: AttrNumber::Long(1), time: Some(Duration::from_millis(250)) }),
            change_event: AttrChangeEventConfig { rel_change: Some(0.5), abs_change: None },
            periodic_event: AttrPeriodicEventConfig { period: Some(Duration::from_millis(1000)) },
            ..AttrConfig::default()
        };

        let info = AttrInfo {
            config,
            mode: AttrMode::ReadWrite,
            data_kind: AttrDataKind::Long,
            data_format: AttrDataFormat::Scalar,
            display_level: DisplayLevel::Operator,
            memorized: None,
        };

        let mut writer = CdrWriter::new();
        write_attr_info(&mut writer, DevVersion::V5, "Position", &info).unwrap();
        let buf = writer.into_buf().freeze();

        let (name, back) = read_attr_info(&mut CdrReader::new(&buf), DevVersion::V5).unwrap();
        assert_eq!(name, "Position");
        assert_eq!(back.unwrap(), info);

    }

    /// A `TimeVal` a sender never filled in — the halves of a pointer, which is what
    /// cppTango leaves in the one it sends with a write — is no time rather than an error,
    /// whichever of the three components is the negative one.
    #[test]
    fn unset_time() {

        for (tv_sec, tv_usec, tv_nsec) in [
            (-116981683, 32749, -116981655),
            (32716, -1409265532, 32716),
            (i32::MIN, i32::MIN, i32::MIN),
            (-1, 0, 0),
        ] {

            let mut writer = CdrWriter::new();
            writer.write_long(tv_sec).unwrap();
            writer.write_long(tv_usec).unwrap();
            writer.write_long(tv_nsec).unwrap();
            let buf = writer.into_buf().freeze();

            let read = CdrReader::new(&buf).read::<SystemTime>()
                .unwrap_or_else(|err| panic!("({tv_sec}, {tv_usec}, {tv_nsec}) was refused: {err}"));

            assert_eq!(read, SystemTime::UNIX_EPOCH, "({tv_sec}, {tv_usec}, {tv_nsec})");

        }

    }

    /// One whose components are each in range but whose total is not answers no time
    /// either, rather than overflowing — which it did, and a debug build panicked on it.
    #[test]
    fn unreachable_time() {
        let mut writer = CdrWriter::new();
        writer.write_long(i32::MAX).unwrap();
        writer.write_long(i32::MAX).unwrap();
        writer.write_long(i32::MAX).unwrap();
        let buf = writer.into_buf().freeze();
        assert!(CdrReader::new(&buf).read::<SystemTime>().is_ok(), "a time too far out was refused");
    }

    /// A time that is one goes out and comes back, to the microsecond `TimeVal` carries.
    #[test]
    fn time_roundtrip() {

        let sent = SystemTime::UNIX_EPOCH + Duration::from_micros(1_788_714_320_192_300);

        let mut writer = CdrWriter::new();
        writer.write(&sent).unwrap();
        let buf = writer.into_buf().freeze();

        assert_eq!(CdrReader::new(&buf).read::<SystemTime>().unwrap(), sent);

    }

    /// Same as above for the other composite type, which only differs by the type of its
    /// first array and by the name that array goes out under.
    #[test]
    fn double_string_array() {

        let data = Value::from((Buf::from(&[1.5f64, 2.5][..]), Buf::from(&["one"][..])));

        let mut writer = CdrWriter::new();
        write_command_data(&mut writer, &data).unwrap();
        let buf = writer.into_buf().freeze();

        let mut reader = CdrReader::new(&buf);
        check_composite_typecode(
            &mut reader,
            tango_repo_id!("DevVarDoubleStringArray"),
            "dvalue",
            tango_repo_id!("DevVarDoubleArray"));

        assert_eq!(read_command_data(&mut CdrReader::new(&buf)).unwrap(), data);

    }

}