Add a sparse_output flag to the histogram config in the registry.

Bug: 102478
Change-Id: I7d82b3b372eff9410d2038678bb62deeda413a40
Reviewed-on: https://fuchsia-review.googlesource.com/c/cobalt/+/691384
Reviewed-by: Laura Peskin <pesk@google.com>
Commit-Queue: Cameron Dale <camrdale@google.com>
diff --git a/src/bin/config_parser/src/config_validator/int_buckets.go b/src/bin/config_parser/src/config_validator/int_buckets.go
index 269b7c8..0101a39 100644
--- a/src/bin/config_parser/src/config_validator/int_buckets.go
+++ b/src/bin/config_parser/src/config_validator/int_buckets.go
@@ -55,16 +55,16 @@
 
 	if r.IntBuckets != nil {
 		// If it exists, the report bucket config takes priority.
-		return validateIntBuckets(*r.IntBuckets)
+		return validateIntBuckets(*r.IntBuckets, r)
 	} else if m.IntBuckets != nil {
-		return validateIntBuckets(*m.IntBuckets)
+		return validateIntBuckets(*m.IntBuckets, r)
 	}
 
 	return nil
 }
 
 // Validate the fields in an int_buckets.
-func validateIntBuckets(bucket config.IntegerBuckets) error {
+func validateIntBuckets(bucket config.IntegerBuckets, r config.ReportDefinition) error {
 	if bucket_value := bucket.GetLinear(); bucket_value != nil {
 		if bucket_value.NumBuckets < 1 {
 			return fmt.Errorf("Linear bucket must set a num_buckets greater than or equal to 1.")
@@ -86,5 +86,9 @@
 		return fmt.Errorf("int_config must define either a linear or exponential bucket definition.")
 	}
 
+	if bucket.SparseOutput && r.PrivacyLevel != config.ReportDefinition_PRIVACY_LEVEL_UNKNOWN && r.PrivacyLevel != config.ReportDefinition_NO_ADDED_PRIVACY {
+		return fmt.Errorf("int_config can not have sparse_output set if additional privacy is enabled on the report.")
+	}
+
 	return nil
 }
diff --git a/src/bin/config_parser/src/config_validator/int_buckets_test.go b/src/bin/config_parser/src/config_validator/int_buckets_test.go
index 7d878c8..93cf58a 100644
--- a/src/bin/config_parser/src/config_validator/int_buckets_test.go
+++ b/src/bin/config_parser/src/config_validator/int_buckets_test.go
@@ -102,70 +102,93 @@
 
 // Tests int_buckets validation.
 func TestValidateIntBuckets(t *testing.T) {
+	r := makeValidReportWithType(config.ReportDefinition_UNIQUE_DEVICE_HISTOGRAMS)
+
 	// Test that an IntBuckets without a linear or exponential bucket defined returns an error.
 	bucket := config.IntegerBuckets{}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("Uninitialized int_buckets should return an error.")
 	}
 
 	// Test that an empty Linear bucket config returns an error.
 	bucket.Buckets = &config.IntegerBuckets_Linear{}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("int_buckets with an empty linear_bucket should return an error.")
 	}
 
 	// Test that a Linear bucket config with invalid num_buckets returns an error.
 	linear := &config.LinearIntegerBuckets{Floor: 0, NumBuckets: 0, StepSize: 10}
 	bucket.Buckets = &config.IntegerBuckets_Linear{Linear: linear}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("A linear_bucket with 0 num_buckets should return an error.")
 	}
 
 	// Test that a Linear bucket config with invalid step_size returns an error.
 	linear = &config.LinearIntegerBuckets{Floor: 0, NumBuckets: 10, StepSize: 0}
 	bucket.Buckets = &config.IntegerBuckets_Linear{Linear: linear}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("A linear_bucket with 0 step_size should return an error.")
 	}
 
 	// Test that a valid Linear bucket config passes.
 	linear = &config.LinearIntegerBuckets{Floor: 0, NumBuckets: 20, StepSize: 10}
 	bucket.Buckets = &config.IntegerBuckets_Linear{Linear: linear}
-	if err := validateIntBuckets(bucket); err != nil {
+	if err := validateIntBuckets(bucket, r); err != nil {
 		t.Errorf("Rejected a valid linear int_config: %v", err)
 	}
 
 	// Test that an empty Exponential bucket config returns an error.
 	bucket.Buckets = &config.IntegerBuckets_Exponential{}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("int_buckets with an empty exponential_bucket should return an error.")
 	}
 
 	// Test that an Exponential bucket config with invalid num_buckets returns an error.
 	exponential := &config.ExponentialIntegerBuckets{Floor: 0, NumBuckets: 0, InitialStep: 10, StepMultiplier: 2}
 	bucket.Buckets = &config.IntegerBuckets_Exponential{Exponential: exponential}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("An exponential_bucket with 0 num_buckets should return an error.")
 	}
 
 	// Test that an Exponential bucket config with invalid initial_step returns an error.
 	exponential = &config.ExponentialIntegerBuckets{Floor: 0, NumBuckets: 10, InitialStep: 0, StepMultiplier: 2}
 	bucket.Buckets = &config.IntegerBuckets_Exponential{Exponential: exponential}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("An exponential_bucket with 0 initial_step should return an error.")
 	}
 
 	// Test that an Exponential bucket config with invalid step_multiplier returns an error.
 	exponential = &config.ExponentialIntegerBuckets{Floor: 0, NumBuckets: 20, InitialStep: 2, StepMultiplier: 0}
 	bucket.Buckets = &config.IntegerBuckets_Exponential{Exponential: exponential}
-	if err := validateIntBuckets(bucket); err == nil {
+	if err := validateIntBuckets(bucket, r); err == nil {
 		t.Errorf("An exponential_bucket with 0 step_multipier should return an error.")
 	}
 
 	// Test that a valid Exponential bucket config passes.
 	exponential = &config.ExponentialIntegerBuckets{Floor: 0, NumBuckets: 30, InitialStep: 2, StepMultiplier: 2}
 	bucket.Buckets = &config.IntegerBuckets_Exponential{Exponential: exponential}
-	if err := validateIntBuckets(bucket); err != nil {
+	if err := validateIntBuckets(bucket, r); err != nil {
 		t.Errorf("Rejected a valid exponential int_config: %v", err)
 	}
+
+	// Test that sparse_output can be set on reports with no additional privacy.
+	linear = &config.LinearIntegerBuckets{Floor: 0, NumBuckets: 20, StepSize: 10}
+	bucket.Buckets = &config.IntegerBuckets_Linear{Linear: linear}
+	bucket.SparseOutput = true
+	if err := validateIntBuckets(bucket, r); err != nil {
+		t.Errorf("Rejected a valid sparse_output setting on a report with no privacy setting: %v", err)
+	}
+	r.PrivacyLevel = config.ReportDefinition_NO_ADDED_PRIVACY
+	if err := validateIntBuckets(bucket, r); err != nil {
+		t.Errorf("Rejected a valid sparse_output setting on a report with no added privacy: %v", err)
+	}
+	// Test that sparse_output can NOT be set on reports WITH additional privacy.
+	r.PrivacyLevel = config.ReportDefinition_LOW_PRIVACY
+	if err := validateIntBuckets(bucket, r); err == nil {
+		t.Errorf("An int_bucket with sparse_output on a report with low privacy should return an error.")
+	}
+	r.PrivacyLevel = config.ReportDefinition_HIGH_PRIVACY
+	if err := validateIntBuckets(bucket, r); err == nil {
+		t.Errorf("An int_bucket with sparse_output on a report with high privacy should return an error.")
+	}
 }
diff --git a/src/registry/report_definition.proto b/src/registry/report_definition.proto
index 6a1a685..c2324c6 100644
--- a/src/registry/report_definition.proto
+++ b/src/registry/report_definition.proto
@@ -1101,6 +1101,12 @@
     ExponentialIntegerBuckets exponential = 1;
     LinearIntegerBuckets linear = 2;
   }
+
+  // If set to true, empty buckets will not be added to the report data such
+  // that all histograms contain a row for every bucket. Buckets with a zero
+  // count may still occur if data is logged that contains a zero count. This
+  // field can not be set on reports with added privacy.
+  bool sparse_output = 3;
 }
 
 message StringSketchParameters {