File Coverage

lib/CodeGen/Protection.pm
Criterion Covered Total %
statement 32 32 100.0
branch n/a
condition n/a
subroutine 11 11 100.0
pod 2 2 100.0
total 45 45 100.0


line stmt bran cond sub pod time code
1             package CodeGen::Protection;
2              
3             # ABSTRACT: Safely rewrite parts of generated code
4              
5 3     3   400532 use v5.10.0; # for named captures in regexes
  3         44  
6 3     3   16 use strict;
  3         7  
  3         57  
7 3     3   15 use warnings;
  3         5  
  3         84  
8 3     3   15 use base 'Exporter';
  3         4  
  3         415  
9 3     3   1646 use Module::Runtime qw( use_module );
  3         5636  
  3         18  
10 3     3   236 use Carp 'croak';
  3         7  
  3         179  
11 3         41 use CodeGen::Protection::Types qw(
12             compile_named
13             NonEmptyStr
14             Bool
15             Optional
16 3     3   1136 );
  3         9  
17              
18             our $VERSION = '0.04';
19             our @EXPORT_OK = qw(
20             create_protected_code
21             rewrite_code
22             );
23             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
24              
25             sub create_protected_code {
26 3     3 1 316 state $check = compile_named(
27             type => NonEmptyStr,
28             protected_code => NonEmptyStr,
29             tidy => Optional [Bool],
30             name => Optional [NonEmptyStr],
31             overwrite => Optional [Bool],
32             );
33 3         11578 return _rewritten( $check->(@_) );
34             }
35              
36             sub rewrite_code {
37 9     9 1 17311 state $check = compile_named(
38             type => NonEmptyStr,
39             protected_code => NonEmptyStr,
40             existing_code => NonEmptyStr,
41             tidy => Optional [Bool],
42             name => Optional [NonEmptyStr],
43             overwrite => Optional [Bool],
44             );
45 9         10753 return _rewritten( $check->(@_) );
46             }
47              
48             sub _rewritten {
49 12     12   579 my $arg_for = shift;
50 12         30 my $type = delete $arg_for->{type};
51 12         35 my $class = _use_module($type);
52 12         251 return $class->new($arg_for)->rewritten;
53             }
54              
55             sub _use_module {
56 12     12   17 my $type = shift;
57 12         34 my $class = "CodeGen::Protection::Format::$type";
58 12         51 use_module($class);
59 12         391 return $class;
60             }
61              
62             1;
63              
64             __END__
65              
66             =pod
67              
68             =encoding UTF-8
69              
70             =head1 NAME
71              
72             CodeGen::Protection - Safely rewrite parts of generated code
73              
74             =head1 VERSION
75              
76             version 0.04
77              
78             =head1 SYNOPSIS
79              
80             use CodeGen::Protection qw(:all);
81              
82             # Creating a new document:
83              
84             my $perl = create_protected_code(
85             type => 'Perl',
86             protected_code => $sample,
87             );
88              
89             # Or rewriting:
90              
91             my $rewritten = rewrite_code(
92             type => 'Perl',
93             existing_code => $perl,
94             protected_code => $rewritten_code,
95             );
96              
97             =head1 DESCRIPTION
98              
99             Code that writes code can be a powerful tool, especially when you need to
100             generate lots of boilerplate. However, when a developer takes the generated
101             code, they can easily rewrite that code in a way that no longer works, or make
102             good changes that get wiped out if the code is regenerated.
103             L<DBIx::Class::Schema::Loader|https://metacpan.org/pod/DBIx::Class::Schema::Loader>
104             protects against this by marking blocks of code with start and end comments
105             and an MD5 checksum. If you change any of the code between those comments,
106             regenerating your schema will fail.
107              
108             This module takes this idea and generalizes it. It allows you to do a safe
109             partial rewrite of documents. At the present time, we support Perl and HTML.
110              
111             In short, we wrap your "protected" (C<protected_code>) code in start and end
112             comments, with checksums for the code:
113              
114             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
115            
116             # protected code goes here
117              
118             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
119              
120             Or:
121              
122             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the end comment. Checksum: c286b9b2577e085df857227eae996c40 -->
123            
124             <ol>
125             <li>This is a list</li>
126             <li>This is the second entry.</li>
127             </ol>
128            
129             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the start comment. Checksum: c286b9b2577e085df857227eae996c40 -->
130              
131             If calling the C<rewrite_code> function, this module removes the code between
132             the C<existing_code>'s start and end markers and replaces it with the
133             C<protected_code>. If the code between the start and end markers has been
134             altered, it will no longer match the checksums and rewriting the code will
135             fail.
136              
137             =head1 TYPES
138              
139             As of this writing, we can protect Perl and HTML:
140              
141             my $rewritten = rewrite_code(
142             type => 'Perl',
143             existing_code => $perl,
144             protected_code => $protected_code,
145             );
146              
147             my $rewritten = rewrite_code(
148             type => 'HTML',
149             existing_code => $HTML,
150             protected_code => $protected_code,
151             );
152              
153             See L<CodeGen::Protection::Role> to learn how to create your own types to protect.
154              
155             =head1 FUNCTIONS
156              
157             Functions are exportable on-demand, or both can be exported via C<:all>.
158              
159             use CodeGen::Protection qw(rewrite_code);
160             use CodeGen::Protection qw(:all);
161              
162             =head2 C<create_protected_code>
163              
164             my $protected_code = create_protected_code(
165             type => 'Perl',
166             protected_code => $text_of_code,
167             );
168              
169             Takes the code in C<$text_of_code> and adds start and end markers to it.
170              
171             =head2 C<rewrite_code>
172              
173             my $protected_code = create_protected_code(
174             type => 'Perl',
175             protected_code => $protected_code,
176             existing_code => $existing_code,
177             );
178              
179             Replaces the code in the protected block of C<$existing_code> with the code
180             from C<$protected_code>.
181              
182             =head3 ARGUMENTS
183              
184             Both C<create_protected_code> and C<rewrite_code> take the same arguments,
185             except that C<rewrite_code> does not allow the C<protected_code> argument.
186              
187             =over 4
188              
189             =item * C<protected_code>
190              
191             This is a required string containing any new Perl code to be built with this
192             tool.
193              
194             =item * C<existing_code>
195              
196             This is an optional string containing Perl code already built with this tool.
197             If provided, this code I<must> have the start and end markers generated by
198             this tool so that the rewriter knows the section of code to replace with the
199             injected code.
200              
201             =item * C<name>
202              
203             Optional name for the code. This is only used in error messages if you're
204             generating a lot of code and an error occurs and you'd like to see the name
205             in the error.
206              
207             =item * C<tidy>
208              
209             If true, will attempt to tidy the C<protected_code> block (the rest of the
210             code is ignored). For Perl, if the value of perltidy is the number 1 (one),
211             then a generic pass of L<Perl::Tidy> will be done on the code. If the value is
212             true and anything I<other> than one, this is assumed to be the path to a
213             F<.perltidyrc> file and that will be used to tidy the code (or C<croak()> if
214             the F<.perltidyrc> file cannot be found).
215              
216             =item * C<overwrite>
217              
218             Optional boolean, default false. In "Rewrite mode", if the checksum in the
219             start and end markers doesn't match the code within them, someone has manually
220             altered that code and we do not automatically overwrite it (in fact, we
221             C<croak()>). Setting C<overwrite> to true will cause it to be overwritten.
222              
223             =back
224              
225             =head1 MODES
226              
227             There are two modes: "Creation" and "Rewrite."
228              
229             =head2 Creation Mode
230              
231             my $protected_code = create_protected_code(
232             protected_code => $text,
233             );
234              
235             This will wrap the new text in start and end tags that "protect" the document
236             if you rewrite it:
237              
238             my $perl = <<'END';
239             sub sum {
240             my $total = 0;
241             $total += $_ foreach @_;
242             return $total;
243             }
244             END
245             my $protected_code = create_protected_code( protected_code => $perl );
246              
247             Result:
248              
249             #<<< CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
250              
251             sub sum {
252             my $total = 0;
253             $total += $_ foreach @_;
254             return $total;
255             }
256              
257             #>>> CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
258              
259             You can then take the marked up document and insert it into another Perl
260             document and use the rewrite mode to safely rewrite the code between the start
261             and end markers. The rest of the document will be ignored.
262              
263             Note that leading and trailing comments start with C<< #<<< >> and C<< #>>> >>
264             respectively. Those are special comments which tell L<Perl::Tidy> to ignore
265             what ever is between them. Thus, you can safely tidy code written with this.
266              
267             The start and end checksums are the same and are the checksum of the text
268             between the comments. Leading and trailing lines which are all whitespace are
269             removed and one leading and one trailing newline will be added.
270              
271             =head2 Rewrite Mode
272              
273             Given a document created with the "Creating" mode, you can then take the
274             marked up document and insert it into another Perl document and use the
275             rewrite mode to safely rewrite the code between the start and end markers.
276             The rest of the document will be ignored.
277              
278             my $rewrite = rewrite_code(
279             existing_code => $existing_code,
280             protected_code => $protected_code,
281             );
282              
283             In the above, assuming that C<$existing_code> is a rewritable document, the
284             C<$protected_code> will replace the rewritable section of the C<$existing_code>, leaving
285             the rest unchanged.
286              
287             However, if C<$protected_code> is I<also> a rewritable document, then the rewritable
288             portion of the C<$protected_code> will be extract and used to replace the rewritable
289             portion of the C<$existing_code>.
290              
291             So for the code shown in the "Creation mode" section, you could add more code
292             like this:
293              
294             package My::Package;
295              
296             use strict;
297             use warnings;
298              
299             sub average {
300             return sum(@_)/@_;
301             }
302              
303             #<<< CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
304              
305             sub sum {
306             my $total = 0;
307             $total += $_ foreach @_;
308             return $total;
309             }
310              
311             #>>> CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
312            
313             1;
314              
315             However, later on I might realize that the C<sum> function will happily try to
316             sum things which are not numbers, so I want to fix that. I'll slurp the
317             C<My::Package> code into the C<$existing_code> variable and then:
318              
319             my $perl = <<'END';
320             use Scalar::Util 'looks_like_number';
321              
322             sub sum {
323             my $total = 0;
324             foreach my $number (@_) {
325             unless (looks_like_number($number)) {
326             die "'$number' doesn't look like a numbeer!";
327             }
328             $total += $number;
329             }
330             return $total;
331             }
332             END
333             my $rewrite = rewrite_code( existing_code => $existing_code, protected_code => $perl );
334              
335             And that will result in:
336              
337             package My::Package;
338            
339             use strict;
340             use warnings;
341            
342             sub average {
343             return sum(@_)/@_;
344             }
345            
346             #<<< CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the end comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
347            
348             use Scalar::Util 'looks_like_number';
349            
350             sub sum {
351             my $total = 0;
352             foreach my $number (@_) {
353             unless (looks_like_number($number)) {
354             die "'$number' doesn't look like a numbeer!";
355             }
356             $total += $number;
357             }
358             return $total;
359             }
360            
361             #>>> CodeGen::Protection::Format::Perl 0.03. Do not touch any code between this and the start comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
362            
363             1;
364              
365             You can see that the code between the start and end checksum comments and been
366             rewritten, while the rest of the code remains unchanged.
367              
368             =head1 ACKNOWLEDGEMENTS
369              
370             We would like to thank L<All Around the World|https://allaroundtheworld.fr/>
371              
372             Thanks to Matt Trout (mst) for the inspiration from the schema loader.
373             for sponsoring this work.
374              
375             =head1 AUTHOR
376              
377             Curtis "Ovid" Poe <ovid@allaroundtheworld.fr>
378              
379             =head1 COPYRIGHT AND LICENSE
380              
381             This software is copyright (c) 2021 by Curtis "Ovid" Poe.
382              
383             This is free software; you can redistribute it and/or modify it under
384             the same terms as the Perl 5 programming language system itself.
385              
386             =cut