• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.platform.test.ravenwood.ravenhelper.policytoannot
17 
18 /*
19  * This file contains classes and functions about file edit operations, such as
20  * "insert a line", "delete a line".
21  */
22 
23 
24 import com.android.hoststubgen.log
25 import java.io.BufferedWriter
26 import java.io.File
27 import java.io.FileOutputStream
28 import java.io.OutputStreamWriter
29 
30 enum class SourceOperationType {
31     /** Insert a line */
32     Insert,
33 
34     /** delete a line */
35     Delete,
36 
37     /** Insert a text at the beginning of a line */
38     Prepend,
39 }
40 
41 data class SourceOperation(
42     /** Target file to edit. */
43     val sourceFile: String,
44 
45     /** 1-based line number. Use -1 to add at the end of the file. */
46     val lineNumber: Int,
47 
48     /** Operation type.*/
49     val type: SourceOperationType,
50 
51     /** Operand -- text to insert or prepend. Ignored for delete. */
52     val text: String = "",
53 
54     /** Human-readable description of why this operation was created */
55     val description: String,
56 ) {
toStringnull57     override fun toString(): String {
58         return "SourceOperation(sourceFile='$sourceFile', " +
59                 "lineNumber=$lineNumber, type=$type, text='$text' desc='$description')"
60     }
61 }
62 
63 /**
64  * Stores list of [SourceOperation]s for each file.
65  */
66 class SourceOperations {
67     var size: Int = 0
68         private set
69     private val fileOperations = mutableMapOf<String, MutableList<SourceOperation>>()
70 
addnull71     fun add(op: SourceOperation) {
72         log.forVerbose {
73             log.v("Adding operation: $op")
74         }
75         size++
76         fileOperations[op.sourceFile]?.let { ops ->
77             ops.add(op)
78             return
79         }
80         fileOperations[op.sourceFile] = mutableListOf(op)
81     }
82 
83     /**
84      * Get the collected [SourceOperation]s for each file.
85      */
getOperationsnull86     fun getOperations(): MutableMap<String, MutableList<SourceOperation>> {
87         return fileOperations
88     }
89 }
90 
91 /**
92  * Create a shell script to apply all the operations (using sed).
93  */
createShellScriptnull94 fun createShellScript(ops: SourceOperations, writer: BufferedWriter) {
95     // File header.
96     // Note ${'$'} is an ugly way to put a dollar sign ($) in a multi-line string.
97     writer.write(
98         """
99         #!/bin/bash
100 
101         set -e # Finish when any command fails.
102 
103         function apply() {
104             local file="${'$'}1"
105 
106             # The script is given via stdin. Write it to file.
107             local sed="/tmp/pta-script.sed.tmp"
108             cat > "${'$'}sed"
109 
110             echo "Running: sed -i -f \"${'$'}sed\" \"${'$'}file\""
111 
112             if ! sed -i -f "${'$'}sed" "${'$'}file" ; then
113                 echo 'Failed!' 1>&2
114                 return 1
115             fi
116         }
117 
118         """.trimIndent()
119     )
120 
121     ops.getOperations().toSortedMap().forEach { (origFile, ops) ->
122         val file = File(origFile).absolutePath
123 
124         writer.write("\n")
125 
126         writer.write("#")
127         writer.write("=".repeat(78))
128         writer.write("\n")
129 
130         writer.write("\n")
131 
132         writer.write("apply \"$file\" <<'__END_OF_SCRIPT__'\n")
133         toSedScript(ops, writer)
134         writer.write("__END_OF_SCRIPT__\n")
135     }
136 
137     writer.write("\n")
138 
139     writer.write("echo \"All files updated successfully!\"\n")
140     writer.flush()
141 }
142 
143 /**
144  * Create a sed script to apply a list of operations.
145  */
toSedScriptnull146 private fun toSedScript(ops: List<SourceOperation>, writer: BufferedWriter) {
147     ops.sortedBy { it.lineNumber }.forEach { op ->
148         if (op.text.contains('\n')) {
149             throw RuntimeException("Operation $op may not contain newlines.")
150         }
151 
152         // Convert each operation to a sed operation. Examples:
153         //
154         // - Insert "abc" to line 2
155         //   2i\
156         //   abc
157         //
158         // - Insert "abc" to the end of the file
159         //   $a\
160         //   abc
161         //
162         // - Delete line 2
163         //   2d
164         //
165         // - Prepend abc to line 2
166         //   2s/^/abc/
167         //
168         // The line numbers are all the line numbers in the original file. Even though
169         // the script itself will change them because of inserts and deletes, we don't need to
170         // change the line numbers in the script.
171 
172         // Write the target line number.
173         writer.write("\n")
174         writer.write("# ${op.description}\n")
175         if (op.lineNumber >= 0) {
176             writer.write(op.lineNumber.toString())
177         } else {
178             writer.write("$")
179         }
180 
181         when (op.type) {
182             SourceOperationType.Insert -> {
183                 if (op.lineNumber >= 0) {
184                     writer.write("i\\\n") // "Insert"
185                 } else {
186                     // If it's the end of the file, we need to use "a" (append)
187                     writer.write("a\\\n")
188                 }
189                 writer.write(op.text)
190                 writer.write("\n")
191             }
192             SourceOperationType.Delete -> {
193                 writer.write("d\n")
194             }
195             SourceOperationType.Prepend -> {
196                 if (op.text.contains('/')) {
197                     TODO("Operation $op contains character(s) that needs to be escaped.")
198                 }
199                 writer.write("s/^/${op.text}/\n")
200             }
201         }
202     }
203 }
204 
createShellScriptnull205 fun createShellScript(ops: SourceOperations, scriptFile: String?): Boolean {
206     if (ops.size == 0) {
207         log.i("No files need to be updated.")
208         return false
209     }
210 
211     val scriptWriter = BufferedWriter(
212         OutputStreamWriter(
213             scriptFile?.let { file ->
214             FileOutputStream(file)
215         } ?: System.out
216     ))
217 
218     scriptWriter.use { writer ->
219         scriptFile?.let {
220             log.i("Creating script file at $it ...")
221         }
222         createShellScript(ops, writer)
223     }
224     return true
225 }