. "$BST_LIB" # libical is the reference implementation of the iCalendar family -- RFC 5545 # (the VCALENDAR object), RFC 5546 (iTIP scheduling) and RFC 6047 (its mail # binding). It is the first calendaring datatype on this image, and it is a # genuinely different shape from every serialisation library already here: # JSON, YAML, INI, CBOR, XML and protobuf all describe a value tree, whereas # iCalendar is a line-oriented, folded, escaped, MIME-descended grammar in which # "the last working Friday of every second month, minus two dates" is a single # property value with an expansion algorithm behind it. # # The distro is static-only, so upstream's ${PN} (the shared objects) is empty # and what the image gets is libical-dev + libical-staticdev: three archives # (libical.a, libicalss.a, libicalvcal.a), 47 headers under /usr/include/libical, # libical.pc, and the cmake package files. # # --------------------------------------------------------------------------- # WHY THIS SUITE RUNS A PROBE # # libical installs NO PROGRAM -- not one binary, on any configuration this port # could have chosen. Without a cross-linked consumer the suite could only # assert that files exist, which would pass identically if every function in the # archives trapped on first call. BRINTOS-217 is why that is not good enough: # oniguruma compiled and packaged perfectly on this arm and then returned # silently wrong offsets to every caller, so a payload-only suite would have # gone green on the exact defect the port existed to find. # # data/libical-probe (src/libical_probe.c) is that consumer: cross-linked with # wasmcc-glibc against THE VERY ARCHIVES THAT SHIP (/usr/lib/libical.a and its # two siblings), reached through the way a real consumer # reaches them. It carries the ticket's pass criterion in group C -- parse a # fixed VCALENDAR, re-emit it, feed the re-emission back through the parser and # emit again, and require the two emissions to be identical. # # HOW THE EXPECTATIONS WERE SET. Every value compared below is the value a # NATIVE x86_64 build of the identical libical 3.0.17 sources, configured with # the identical cmake switches (STATIC_ONLY, no C++ bindings, no glib, no ICU, # no BDB), produces from the same probe source -- captured as # data/libical-native.txt. The probe prints only what cannot legitimately # differ between two machines: counts, byte lengths, normalised rcs, and the # literal text libical itself emits. No pointers, no pids, no sizeof, and # nothing that reads the current time -- every date in it is a literal. # # THE ANCHORED CASES EXIST BECAUSE AN ORACLE CAN BE WRONG. A control captured # from an already-broken build would make the bulk comparison agree with itself, # so the values the RFCs fix -- the 75-octet fold, the CRLF terminator, the # escaping, the recurrence expansion, the leap-year arithmetic -- are ALSO # asserted against literals further down. Those hold whatever the control says. # # WHAT IS DELIBERATELY NOT HERE. RSCALE (RFC 7529, non-Gregorian recurrence) # needs ICU, which this image does not carry and this port does not add. That # is not silently omitted: probe case H03 asserts that libical REFUSES an RSCALE # rule rather than expanding it wrongly, which is the correct failure and the # one that matters. Gregorian recurrence -- group D, and the 99.9% of real # iCalendar -- is unaffected. # --------------------------------------------------------------------------- lib=/usr/lib/libical.a libss=/usr/lib/libicalss.a libvcal=/usr/lib/libicalvcal.a incd=/usr/include/libical pc=/usr/lib/pkgconfig/libical.pc cmd=/usr/lib/cmake/LibIcal DATA=$BST_DATA NATIVE=$DATA/libical-native.txt nl=' ' if [ ! -f "$lib" ]; then bst_skip libical/present "libical not installed on this image" exit 0 fi bst_ok libical/present hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } # --- payload shape ----------------------------------------------------------- # Three archives, because libical is three libraries: the core object model and # parser (libical), the storage/query layer (libicalss), and the vCalendar 1.0 # reader that converts the old format forward (libicalvcal). Group J drives one # entry point from each of the latter two, so a missing archive is a link error # in the probe rather than only a missing file here. hasfile libical/archive "$lib" hasfile libical/archive-ss "$libss" hasfile libical/archive-vcal "$libvcal" hasfile libical/pkgconfig "$pc" hasfile libical/cmake-config "$cmd/LibIcalConfig.cmake" hasfile libical/cmake-version "$cmd/LibIcalConfigVersion.cmake" hasfile libical/cmake-targets "$cmd/LibIcalTargets.cmake" for h in ical.h icalcomponent.h icalproperty.h icalparameter.h icalvalue.h \ icalparser.h icalrecur.h icaltime.h icaltimezone.h icalduration.h \ icalperiod.h icalerror.h icalenums.h icalmemory.h icalrestriction.h \ icalss.h icalgauge.h icalvcal.h vobject.h vcc.h; do hasfile "libical/header-${h%.h}" "$incd/$h" done # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape libical/no-shared-object "/usr/lib/libical.so*" \ "/usr/lib/libicalss.so*" "/usr/lib/libicalvcal.so*" # The glib binding is the PACKAGECONFIG this port drops, and it is a whole # second API surface: dropping it is what makes libical dependency-free here, so # its absence is asserted rather than assumed. If a later recipe change turns # ICAL_GLIB back on, this says so instead of the image quietly growing glib. if [ -e /usr/lib/libical-glib.a ] || [ -e /usr/include/libical-glib ]; then bst_notok libical/no-glib-binding "libical-glib shipped -- the glib PACKAGECONFIG is back on" else bst_ok libical/no-glib-binding fi # The bbappend stages one empty Ical*.cmake file so oe-core's do_install sed # finds a match with glib off, and removes it again. Nothing about it may reach # a package. if [ -e "$cmd/IcalBrintosGlobStub.cmake" ]; then bst_notok libical/no-glob-stub "the do_install glob stub shipped into libical-dev" else bst_ok libical/no-glob-stub fi # --- the .pc file, i.e. what a consumer's build would read ------------------- pctext=$(cat "$pc" 2>/dev/null) has libical/pc-name "Name: libical" "$pctext" has libical/pc-version "Version: 3.0.17" "$pctext" has libical/pc-libs "-lical" "$pctext" has libical/pc-libs-ss "-licalss" "$pctext" has libical/pc-libs-vc "-licalvcal" "$pctext" has libical/pc-libdir "libdir=\${exec_prefix}/lib" "$pctext" # --- the header's own statement of what it is ------------------------------- icalh=$(cat "$incd/ical.h" 2>/dev/null) has libical/hdr-package "#define ICAL_PACKAGE \"libical\"" "$icalh" has libical/hdr-version "#define ICAL_VERSION \"3.0\"" "$icalh" # The component/property/value kind enumerations ARE the API: a consumer names # these constants, so a header that lost one is a source-incompatible change # however well the archive links. for e in ICAL_VCALENDAR_COMPONENT ICAL_VEVENT_COMPONENT ICAL_VTODO_COMPONENT \ ICAL_VJOURNAL_COMPONENT ICAL_VTIMEZONE_COMPONENT ICAL_VALARM_COMPONENT \ ICAL_VFREEBUSY_COMPONENT; do has "libical/enum-${e#ICAL_}" "$e" "$icalh" done for e in ICAL_SUMMARY_PROPERTY ICAL_DTSTART_PROPERTY ICAL_RRULE_PROPERTY \ ICAL_EXDATE_PROPERTY ICAL_ATTENDEE_PROPERTY ICAL_XLICERROR_PROPERTY; do has "libical/prop-${e#ICAL_}" "$e" "$icalh" done has libical/recur-freq "ICAL_WEEKLY_RECURRENCE" "$icalh" has libical/recur-struct "struct icalrecurrencetype" "$icalh" has libical/time-struct "struct icaltimetype" "$icalh" # --- every entry point the headers declare is in the archives ---------------- # Read out of the headers THAT SHIP rather than from a hardcoded list, so a # release that adds an entry point is checked rather than permanently forgiven. # # Only DECLARATIONS may be harvested, so the awk pass drops three things that # are not one and that a bare "$name(" grep otherwise reads as one: comment # lines -- icalenums.h carries the eight deprecated icalenum_* prototypes # commented out above the #define that replaced each; preprocessor lines AND # their backslash continuations -- icalerror.h:344 calls icalerror_warn() from # inside another macro's body; and the leading * of a block comment. arcsyms=$(cat "$lib" "$libss" "$libvcal" 2>/dev/null | grep -ao 'ical[a-z0-9_]*' | sort -u) decls=$(cat "$incd/ical.h" "$incd/icalss.h" 2>/dev/null | awk ' cont { if ($0 !~ /\\$/) cont = 0; next } /^[ \t]*#/ { if ($0 ~ /\\$/) cont = 1; next } /^[ \t]*(\/\*|\/\/|\*)/ { next } { print }') hdrsyms=$(printf '%s\n' "$decls" \ | grep -oE '\bical[a-z0-9_]*[[:space:]]*\(' \ | sed -e 's/[[:space:]]*(//' | sort -u) # "icalsetiter (*icalset_begin_component)(...)" is a function-pointer MEMBER of # struct icalset: what precedes the "(" is its return type, not an entry point. fnptr=$(printf '%s\n' "$decls" \ | grep -oE '\bical[a-z0-9_]*[[:space:]]*\([[:space:]]*\*' \ | sed -e 's/[[:space:]]*([[:space:]]*\*//' | sort -u) # icalset.h:90 exports icalset_register_class unconditionally, but icalset.c # only compiles it inside #if defined(_DLOPEN_TEST) (lines 135-238), which no # default configuration of 3.0.17 defines. Upstream's header/build mismatch, # identical on a native x86_64 build -- not something this arm can supply. notbuilt="icalset_register_class" nsym=0; nmiss=0; missing="" for f in $hdrsyms; do case "$nl$fnptr$nl" in *"$nl$f$nl"*) continue ;; esac case "$nl$notbuilt$nl" in *"$nl$f$nl"*) continue ;; esac nsym=$((nsym + 1)) case "$nl$arcsyms$nl" in *"$nl$f$nl"*) ;; *) nmiss=$((nmiss + 1)); missing="$missing $f" ;; esac done if [ "$nsym" -ge 200 ]; then bst_ok libical/symbols-counted else bst_notok libical/symbols-counted "only $nsym ical* entry points found in the headers"; fi if [ "$nmiss" -eq 0 ]; then bst_ok libical/symbols-complete else bst_notok libical/symbols-complete "$nmiss of $nsym missing:$(_bst_1line "$missing")"; fi # One name from each translation unit that matters, so a partially-linked # archive is legible rather than merely "incomplete". hasline libical/sym-parser icalparser_parse_string "$arcsyms" # icalparser.c hasline libical/sym-emit icalcomponent_as_ical_string_r "$arcsyms" # icalcomponent.c hasline libical/sym-recur-iter icalrecur_iterator_next "$arcsyms" # icalrecur.c hasline libical/sym-time icaltime_as_ical_string_r "$arcsyms" # icaltime.c hasline libical/sym-tz icaltimezone_get_builtin_timezone "$arcsyms" # icaltimezone.c hasline libical/sym-tzutil icaltzutil_fetch_timezone "$arcsyms" # icaltz-util.c hasline libical/sym-restriction icalrestriction_check "$arcsyms" # icalrestriction.c hasline libical/sym-gauge icalgauge_new_from_sql "$arcsyms" # icalgauge.c (libicalss) hasline libical/sym-vcal icalvcal_convert "$arcsyms" # icalvcal.c (libicalvcal) # --- the library RUNNING, via the cross-linked probe ------------------------- if [ ! -s "$NATIVE" ]; then bst_notok libical/native-present "$NATIVE missing -- no oracle to compare against" exit 0 fi bst_ok libical/native-present # do_install stages data/* at 0644 and the deployed rootfs is read-only at # browser runtime, so the probe cannot be executed where it lies. tmp=${TMPDIR:-/tmp}/bst-libical.$$ _p=$tmp.bin cp "$DATA/libical-probe" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok libical/probe-exec "could not stage an executable copy of $DATA/libical-probe" rm -f "$_p" exit 0 fi bst_ok libical/probe-exec # Bounded: nothing in the probe blocks (no fd waits, no timers, no sleeps), so a # timeout here means a trap or a spin, not slow work. timeout 120 "$_p" > "$tmp.out" 2>"$tmp.err"; prc=$? rm -f "$_p" check_eq libical/probe-rc 0 "$prc" # A truncated run must be distinguishable from a run that legitimately produced # these lines: without a terminator, a death half way through would silently # reduce the number of compared cases rather than fail one. if [ "$(tail -n 1 "$tmp.out" 2>/dev/null)" = "Z01-end ok" ]; then bst_ok libical/probe-complete else bst_notok libical/probe-complete "no terminator; last saw: $(_bst_1line "$(tail -n 2 "$tmp.out" 2>/dev/null)")" fi # --- tag-by-tag against the native control ----------------------------------- # One awk over both files rather than a shell loop of ~200 awk calls: process # spawn is expensive on this target and the whole comparison is in-memory. awk ' NR == FNR { t = $1; $1 = ""; sub(/^ /, ""); W[t] = $0; order[++n] = t; next } { t = $1; $1 = ""; sub(/^ /, ""); G[t] = $0; seen[t] = 1 } END { for (i = 1; i <= n; i++) { t = order[i] if (!(t in seen)) { printf "BST not ok libical/cmp-%s : absent from target output\n", t; continue } if (G[t] == W[t]) printf "BST ok libical/cmp-%s\n", t else printf "BST not ok libical/cmp-%s : want:%s got:%s\n", t, W[t], G[t] } } ' "$NATIVE" "$tmp.out" # --- anchored cases: what the RFCs fix, whatever the control says ------------ # The comparison above is only as good as the oracle. These read the target's # own output and check it against values no correct implementation may change, # so a control captured from a broken build cannot make them agree. lc_get() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$tmp.out"; } # RFC 5545 3.1: content lines are CRLF-terminated and folded so that no line # exceeds 75 octets. The probe's document carries a 123-character SUMMARY # precisely so the emitter has to fold something. check_eq libical/rfc-crlf ok "$(lc_get C04-emit-crlf)" check_eq libical/rfc-folded ok "$(lc_get C06-emit-folded)" check_eq libical/rfc-summary-len 123 "$(lc_get B13-summary-len)" longest=$(lc_get C05-emit-longest-line) if [ -n "$longest" ] && [ "$longest" -le 75 ] 2>/dev/null; then bst_ok libical/rfc-fold-bound else bst_notok libical/rfc-fold-bound "longest emitted line is '$longest' octets, RFC 5545 allows 75" fi # THE PASS CRITERION the ticket names: parse a fixed .ics and re-emit it, with # the re-emission proven by feeding it back through the parser and requiring the # second emission to be byte-identical to the first. check_eq libical/roundtrip-parsed ok "$(lc_get B01-parsed)" check_eq libical/roundtrip-reparsed ok "$(lc_get C07-reparsed)" check_eq libical/roundtrip-identical ok "$(lc_get C09-roundtrip-identical)" check_eq libical/roundtrip-no-errors 0 "$(lc_get C10-reparse-errors)" check_eq libical/roundtrip-records 61 "$(lc_get C13-emit-record-count)" # RFC 5545 3.3.11: in a TEXT value \n is a line break and \, \; are the literal # characters. The parser must decode them and the emitter must re-encode them, # which is why the round-trip above is the real assertion and this is the # readable one. check_eq libical/text-escapes "First line\\nSecond line, with a comma; and a semicolon" \ "$(lc_get B14-description)" check_eq libical/text-comma "Room 5, Building B" "$(lc_get B15-location)" # RFC 5545 3.8.5.3: BYDAY=-1FR is "the last Friday". These five dates are the # last Fridays of Jan-May 2024 and are facts about the calendar, not about any # implementation. check_eq libical/recur-last-friday-1 20240126T120000Z "$(lc_get D05-occ1)" check_eq libical/recur-last-friday-2 20240223T120000Z "$(lc_get D05-occ2)" check_eq libical/recur-last-friday-3 20240329T120000Z "$(lc_get D05-occ3)" check_eq libical/recur-last-friday-4 20240426T120000Z "$(lc_get D05-occ4)" check_eq libical/recur-last-friday-5 20240531T120000Z "$(lc_get D05-occ5)" check_eq libical/recur-count 5 "$(lc_get D06-occurrences)" # FREQ=DAILY;INTERVAL=3;UNTIL=20240115 from the 1st is 1,4,7,10,13 and stops. check_eq libical/recur-interval-1 20240101T120000Z "$(lc_get D09-daily1)" check_eq libical/recur-interval-5 20240113T120000Z "$(lc_get D09-daily5)" check_eq libical/recur-until-stops 5 "$(lc_get D10-daily-count)" # RFC 5545 3.3.6: P1DT2H3M4S is 93784 seconds. Arithmetic, not opinion. check_eq libical/duration-seconds 93784 "$(lc_get E01-duration-seconds)" check_eq libical/duration-emit P1DT2H3M4S "$(lc_get E02-duration-roundtrip)" check_eq libical/duration-negative -PT1H30M "$(lc_get E03-duration-negative)" check_eq libical/utcoffset -18000 "$(lc_get E06-utcoffset)" # 2024 is a leap year: 29 February is day 60 and a Thursday (icaltime_day_of_week # numbers Sunday as 1, so Thursday is 5). check_eq libical/leap-doy 60 "$(lc_get F05-doy)" check_eq libical/leap-dow 5 "$(lc_get F04-dow)" check_eq libical/leap-feb-days 29 "$(lc_get F07-days-in-feb-2024)" check_eq libical/nonleap-feb 28 "$(lc_get F08-days-in-feb-2023)" check_eq libical/leap-year-days 366 "$(lc_get F09-days-in-year-2024)" check_eq libical/leap-plus-one 20240301T120000Z "$(lc_get F10-leap-day-plus-one)" check_eq libical/day-seconds 86400 "$(lc_get F13-subtract-seconds)" # A malformed value must produce an X-LIC-ERROR property, not a crash and not a # silently dropped line -- that substitution IS libical's error contract, and it # is what a consumer checks. check_eq libical/err-not-null ok "$(lc_get G01-bad-parsed-not-null)" check_eq libical/err-count 1 "$(lc_get G02-error-count)" check_eq libical/err-xlic ok "$(lc_get G03-xlicerror-present)" check_eq libical/err-strippable 0 "$(lc_get G05-after-strip)" check_eq libical/err-garbage ok "$(lc_get G06-garbage-rejected)" # The ICU boundary, asserted from the target rather than argued from the recipe: # an RSCALE rule parses (it is a rule part like any other) and is then REFUSED # by the iterator, which is the correct failure for a build with no ICU. check_eq libical/rscale-parses CHINESE "$(lc_get H02-rscale-value)" check_eq libical/rscale-refused ok "$(lc_get H03-rscale-refused-without-icu)" # System tzdata. USE_BUILTIN_TZDATA is off, so libical reads the TZif files the # image's tzdata package installs -- the one thing in this port that depends on # something outside libical, and therefore the one worth asserting rather than # assuming. The two offsets are US federal law, not a database opinion. check_eq libical/tz-found ok "$(lc_get I01-zone-found)" check_eq libical/tz-location America/New_York "$(lc_get I02-zone-tzid)" check_eq libical/tz-summer -14400 "$(lc_get I03-offset-summer)" check_eq libical/tz-summer-dst 1 "$(lc_get I04-summer-is-dst)" check_eq libical/tz-winter -18000 "$(lc_get I05-offset-winter)" check_eq libical/tz-winter-dst 0 "$(lc_get I06-winter-is-dst)" check_eq libical/tz-unknown-null ok "$(lc_get I09-unknown-zone-null)" # libicalss and libicalvcal, i.e. the two archives beyond the core. The gauge # pair matters more than either half: a query that matches AND a query that does # not is evidence the SQL is evaluated, where one match alone would also be # produced by a function that returns a constant. check_eq libical/ss-gauge-match 1 "$(lc_get J02-gauge-matches)" check_eq libical/ss-gauge-reject 0 "$(lc_get J02b-gauge-rejects)" check_eq libical/vcal-converted ok "$(lc_get J04-vcal-converted)" check_eq libical/vcal-summary "Converted from vCalendar 1.0" "$(lc_get J06-vcal-summary)" rm -f "$tmp.out" "$tmp.err"