util: optional MustBeFresh in SegmentFetcher's initial Interest

Change-Id: Icc2d029a6a7a0d634d4502a43083b986ee4e803c
diff --git a/ndn-cxx/util/segment-fetcher.cpp b/ndn-cxx/util/segment-fetcher.cpp
index 11cc339..0d75668 100644
--- a/ndn-cxx/util/segment-fetcher.cpp
+++ b/ndn-cxx/util/segment-fetcher.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California,
+ * Copyright (c) 2013-2024 Regents of the University of California,
  *                         Colorado State University,
  *                         University Pierre & Marie Curie, Sorbonne University.
  *
@@ -107,7 +107,12 @@
 {
   Interest interest(baseInterest);
   interest.setCanBePrefix(true);
-  interest.setMustBeFresh(true);
+  if (!interest.getName().empty() && interest.getName()[-1].isVersion()) {
+    interest.setMustBeFresh(false);
+  }
+  else {
+    interest.setMustBeFresh(m_options.probeLatestVersion);
+  }
   interest.setInterestLifetime(m_options.interestLifetime);
   if (isRetransmission) {
     interest.refreshNonce();
@@ -375,15 +380,15 @@
     return signalError(INTEREST_TIMEOUT, "Timeout exceeded");
   }
 
-  name::Component lastNameComponent = origInterest.getName().get(-1);
+  BOOST_ASSERT(!m_pendingSegments.empty());
+
+  const auto& origName = origInterest.getName();
   std::map<uint64_t, PendingSegment>::iterator pendingSegmentIt;
-  BOOST_ASSERT(m_pendingSegments.size() > 0);
-  if (lastNameComponent.isSegment()) {
-    BOOST_ASSERT(m_pendingSegments.count(lastNameComponent.toSegment()) > 0);
-    pendingSegmentIt = m_pendingSegments.find(lastNameComponent.toSegment());
+  if (!origName.empty() && origName[-1].isSegment()) {
+    pendingSegmentIt = m_pendingSegments.find(origName[-1].toSegment());
+    BOOST_ASSERT(pendingSegmentIt != m_pendingSegments.end());
   }
   else { // First Interest
-    BOOST_ASSERT(m_pendingSegments.size() > 0);
     pendingSegmentIt = m_pendingSegments.begin();
   }
 
@@ -393,7 +398,7 @@
 
   m_rttEstimator.backoffRto();
 
-  if (m_receivedSegments.size() == 0) {
+  if (m_receivedSegments.empty()) {
     // Resend first Interest (until maximum receive timeout exceeded)
     fetchFirstSegment(origInterest, true);
   }
diff --git a/ndn-cxx/util/segment-fetcher.hpp b/ndn-cxx/util/segment-fetcher.hpp
index 6ddf6fe..1312de6 100644
--- a/ndn-cxx/util/segment-fetcher.hpp
+++ b/ndn-cxx/util/segment-fetcher.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2024 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -34,22 +34,29 @@
 namespace ndn {
 
 /**
- * @brief Utility class to fetch the latest version of a segmented object.
+ * @brief Utility class to fetch a versioned and segmented object.
  *
  * SegmentFetcher assumes that segments in the object are named `/<prefix>/<version>/<segment>`,
  * where:
- * - `<prefix>` is the specified prefix,
- * - `<version>` is an unknown version that needs to be discovered, and
- * - `<segment>` is a segment number (the number of segments in the object is unknown until a Data
- *   packet containing the `FinalBlockId` field is received).
+ * - `<prefix>` is an arbitrary name prefix;
+ * - `<version>` is the version number (VersionNameComponent);
+ * - `<segment>` is the segment number (SegmentNameComponent).
+ *
+ * The number of segments in the object is generally unknown until a Data packet containing
+ * a `FinalBlockId` field is received and validated.
+ *
+ * The version can either be provided by the application or be discovered at the beginning
+ * of the fetching process. By default, SegmentFetcher will attempt to probe the latest
+ * version of the object by requesting only "fresh" segments during the initial discovery
+ * phase. This behavior can be turned off by setting Options::probeLatestVersion to false.
  *
  * SegmentFetcher implements the following logic:
  *
- * 1. Express an Interest to discover the latest version of the object:
+ * 1. If the application does not provide a `<version>` component and requires probing the
+ *    latest version of the object, an Interest with CanBePrefix and MustBeFresh is sent to
+ *    discover a fresh version. Otherwise, only CanBePrefix is set.
  *
- *    Interest: `/<prefix>?CanBePrefix&MustBeFresh`
- *
- * 2. Infer the latest version of the object: `<version> = Data.getName().get(-2)`.
+ * 2. Infer the version of the object: `version = data.getName().get(-2).toVersion()`.
  *
  * 3. Keep sending Interests for future segments until an error occurs or the number of segments
  *    indicated by the FinalBlockId in a received Data packet is reached. This retrieval will start
@@ -108,6 +115,7 @@
   public:
     time::milliseconds interestLifetime = 4_s; ///< lifetime of sent Interests - independent of Interest timeout
     time::milliseconds maxTimeout = 60_s; ///< maximum allowed time between successful receipt of segments
+    bool probeLatestVersion = true; ///< use the first Interest to probe the latest version of the object
     bool inOrder = false; ///< true for 'in order' mode, false for 'block' mode
     bool useConstantInterestTimeout = false; ///< if true, Interest timeout is kept at `maxTimeout`
     bool useConstantCwnd = false; ///< if true, window size is kept at `initCwnd`
@@ -129,11 +137,13 @@
    *
    * @param face         Reference to the Face that should be used to fetch data.
    * @param baseInterest Interest for the initial segment of requested data.
-   *                     This interest may include a custom InterestLifetime and parameters that
-   *                     will propagate to all subsequent Interests. The only exception is that the
-   *                     initial Interest will be forced to include the "CanBePrefix=true" and
-   *                     "MustBeFresh=true" parameters, which will not be included in subsequent
-   *                     Interests.
+   *                     This Interest may include certain fields, such as ForwardingHint, that
+   *                     will propagate to all subsequent Interests sent by this SegmentFetcher.
+   *                     As a special case, the initial Interest will be forced to include the
+   *                     CanBePrefix field, which will not be included in subsequent Interests.
+   *                     If Options::probeLatestVersion is true, the initial Interest will also
+   *                     be forced to include the MustBeFresh field, while all subsequent Interests
+   *                     will not include it.
    * @param validator    Reference to the Validator the fetcher will use to validate data.
    *                     The caller must ensure the validator remains valid until either #onComplete
    *                     or #onError has been signaled.
diff --git a/tests/unit/util/segment-fetcher.t.cpp b/tests/unit/util/segment-fetcher.t.cpp
index 567918a..126dfe5 100644
--- a/tests/unit/util/segment-fetcher.t.cpp
+++ b/tests/unit/util/segment-fetcher.t.cpp
@@ -39,9 +39,8 @@
   static shared_ptr<Data>
   makeDataSegment(const Name& baseName, uint64_t segment, bool isFinal)
   {
-    const uint8_t buffer[] = "Hello, world!";
     auto data = makeData(Name(baseName).appendSegment(segment));
-    data->setContent(buffer);
+    data->setContent("Hello, world!\0"sv);
     data->setFreshnessPeriod(1_s);
     if (isFinal) {
       data->setFinalBlock(data->getName()[-1]);
@@ -176,6 +175,32 @@
                     std::invalid_argument);
 }
 
+BOOST_AUTO_TEST_CASE(BasicSingleSegment)
+{
+  DummyValidator acceptValidator;
+  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
+                                                             acceptValidator);
+  connectSignals(fetcher);
+
+  advanceClocks(10_ms);
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  const Interest& interest = face.sentInterests[0];
+  BOOST_CHECK_EQUAL(interest.getName(), "/hello/world");
+  BOOST_CHECK_EQUAL(interest.getMustBeFresh(), true);
+  BOOST_CHECK_EQUAL(interest.getCanBePrefix(), true);
+
+  face.receive(*makeDataSegment("/hello/world/version0", 0, true));
+  advanceClocks(10_ms);
+
+  BOOST_CHECK_EQUAL(nErrors, 0);
+  BOOST_CHECK_EQUAL(nCompletions, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentReceived, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentValidated, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentNacked, 0);
+  BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 0);
+}
+
 BOOST_AUTO_TEST_CASE(ExceedMaxTimeout)
 {
   DummyValidator acceptValidator;
@@ -209,27 +234,6 @@
   BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 1);
 }
 
-BOOST_AUTO_TEST_CASE(BasicSingleSegment)
-{
-  DummyValidator acceptValidator;
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             acceptValidator);
-  connectSignals(fetcher);
-
-  advanceClocks(10_ms);
-
-  face.receive(*makeDataSegment("/hello/world/version0", 0, true));
-
-  advanceClocks(10_ms);
-
-  BOOST_CHECK_EQUAL(nErrors, 0);
-  BOOST_CHECK_EQUAL(nCompletions, 1);
-  BOOST_CHECK_EQUAL(nAfterSegmentReceived, 1);
-  BOOST_CHECK_EQUAL(nAfterSegmentValidated, 1);
-  BOOST_CHECK_EQUAL(nAfterSegmentNacked, 0);
-  BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 0);
-}
-
 BOOST_AUTO_TEST_CASE(ConstantCwnd)
 {
   SegmentFetcher::Options options;
@@ -245,31 +249,30 @@
   BOOST_CHECK_EQUAL(fetcher->m_nSegmentsInFlight, 1);
 
   face.receive(*makeDataSegment("/hello/world/version0", 0, false));
-
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(fetcher->m_cwnd, 1.0);
   BOOST_CHECK_EQUAL(fetcher->m_nSegmentsInFlight, 1);
   BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 2);
   BOOST_CHECK_EQUAL(face.sentInterests.back().getName().get(-1).toSegment(), 1);
-  face.receive(*makeDataSegment("/hello/world/version0", 1, false));
 
+  face.receive(*makeDataSegment("/hello/world/version0", 1, false));
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(fetcher->m_cwnd, 1.0);
   BOOST_CHECK_EQUAL(fetcher->m_nSegmentsInFlight, 1);
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 3);
   BOOST_CHECK_EQUAL(face.sentInterests.back().getName().get(-1).toSegment(), 2);
-  face.receive(*makeDataSegment("/hello/world/version0", 2, false));
 
+  face.receive(*makeDataSegment("/hello/world/version0", 2, false));
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(fetcher->m_cwnd, 1.0);
   BOOST_CHECK_EQUAL(fetcher->m_nSegmentsInFlight, 1);
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 4);
   BOOST_CHECK_EQUAL(face.sentInterests.back().getName().get(-1).toSegment(), 3);
-  face.receive(*makeDataSegment("/hello/world/version0", 3, false));
 
+  face.receive(*makeDataSegment("/hello/world/version0", 3, false));
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(fetcher->m_cwnd, 1.0);
@@ -284,8 +287,8 @@
   BOOST_CHECK_EQUAL(fetcher->m_nSegmentsInFlight, 1);
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 6);
   BOOST_CHECK_EQUAL(face.sentInterests.back().getName().get(-1).toSegment(), 4);
-  face.receive(*makeDataSegment("/hello/world/version0", 4, true));
 
+  face.receive(*makeDataSegment("/hello/world/version0", 4, true));
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(nErrors, 0);
@@ -303,12 +306,20 @@
   nSegments = 401;
   sendNackInsteadOfDropping = false;
 
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             acceptValidator);
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), acceptValidator);
   face.onSendInterest.connect(std::bind(&SegmentFetcherFixture::onInterest, this, _1));
   connectSignals(fetcher);
 
   face.processEvents(1_s);
+  BOOST_REQUIRE_GE(face.sentInterests.size(), 2);
+
+  BOOST_CHECK_EQUAL(face.sentInterests[0].getName(), "/hello/world");
+  BOOST_CHECK_EQUAL(face.sentInterests[0].getMustBeFresh(), true);
+  BOOST_CHECK_EQUAL(face.sentInterests[0].getCanBePrefix(), true);
+  BOOST_CHECK_EQUAL(face.sentInterests[1].getName().size(), 4);
+  BOOST_CHECK(face.sentInterests[1].getName().at(-1).isSegment());
+  BOOST_CHECK_EQUAL(face.sentInterests[1].getMustBeFresh(), false);
+  BOOST_CHECK_EQUAL(face.sentInterests[1].getCanBePrefix(), false);
 
   BOOST_CHECK_EQUAL(nErrors, 0);
   BOOST_CHECK_EQUAL(nCompletions, 1);
@@ -344,6 +355,58 @@
   BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 0);
 }
 
+BOOST_AUTO_TEST_CASE(VersionedPrefix)
+{
+  DummyValidator acceptValidator;
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world/v=42"), acceptValidator);
+  connectSignals(fetcher);
+
+  advanceClocks(1_ms);
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  const Interest& interest = face.sentInterests[0];
+  BOOST_CHECK_EQUAL(interest.getName(), "/hello/world/v=42");
+  BOOST_CHECK_EQUAL(interest.getCanBePrefix(), true);
+  BOOST_CHECK_EQUAL(interest.getMustBeFresh(), false);
+
+  face.receive(*makeDataSegment("/hello/world/v=42", 0, true));
+  advanceClocks(10_ms);
+
+  BOOST_CHECK_EQUAL(nErrors, 0);
+  BOOST_CHECK_EQUAL(nCompletions, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentReceived, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentValidated, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentNacked, 0);
+  BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 0);
+}
+
+BOOST_AUTO_TEST_CASE(ProbeLatestVersionDisabled)
+{
+  DummyValidator acceptValidator;
+  SegmentFetcher::Options options;
+  options.probeLatestVersion = false;
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), acceptValidator, options);
+  connectSignals(fetcher);
+
+  advanceClocks(1_ms);
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+
+  const Interest& interest = face.sentInterests[0];
+  BOOST_CHECK_EQUAL(interest.getName(), "/hello/world");
+  BOOST_CHECK_EQUAL(interest.getCanBePrefix(), true);
+  BOOST_CHECK_EQUAL(interest.getMustBeFresh(), false);
+
+  face.receive(*makeDataSegment("/hello/world/v=0", 0, true));
+  advanceClocks(10_ms);
+
+  BOOST_CHECK_EQUAL(nErrors, 0);
+  BOOST_CHECK_EQUAL(nCompletions, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentReceived, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentValidated, 1);
+  BOOST_CHECK_EQUAL(nAfterSegmentNacked, 0);
+  BOOST_CHECK_EQUAL(nAfterSegmentTimedOut, 0);
+}
+
 BOOST_AUTO_TEST_CASE(FirstSegmentNotZero)
 {
   DummyValidator acceptValidator;
@@ -620,8 +683,7 @@
   sendNackInsteadOfDropping = true;
   nackReason = lp::NackReason::DUPLICATE;
 
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             acceptValidator);
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), acceptValidator);
   face.onSendInterest.connect(std::bind(&SegmentFetcherFixture::onInterest, this, _1));
   connectSignals(fetcher);
 
@@ -645,8 +707,7 @@
   sendNackInsteadOfDropping = true;
   nackReason = lp::NackReason::CONGESTION;
 
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             acceptValidator);
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), acceptValidator);
   face.onSendInterest.connect(std::bind(&SegmentFetcherFixture::onInterest, this, _1));
   connectSignals(fetcher);
 
@@ -669,8 +730,7 @@
   nackReason = lp::NackReason::NO_ROUTE;
   face.onSendInterest.connect(std::bind(&SegmentFetcherFixture::onInterest, this, _1));
 
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             acceptValidator);
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), acceptValidator);
   connectSignals(fetcher);
 
   face.processEvents(1_s);
@@ -690,8 +750,7 @@
   validator.getPolicy().setResultCallback([] (const Name& name) {
     return name.at(-1).toSegment() % 2 == 0;
   });
-  shared_ptr<SegmentFetcher> fetcher = SegmentFetcher::start(face, Interest("/hello/world"),
-                                                             validator);
+  auto fetcher = SegmentFetcher::start(face, Interest("/hello/world"), validator);
   connectSignals(fetcher);
 
   auto data1 = makeDataSegment("/hello/world", 0, false);
@@ -787,7 +846,6 @@
   BOOST_CHECK_EQUAL(weakFetcher.expired(), false);
 
   face.receive(*makeDataSegment("/hello/world/version0", 0, true));
-
   advanceClocks(10_ms);
 
   BOOST_CHECK_EQUAL(nErrors, 0);